Compare commits

..

1 Commits

Author SHA1 Message Date
juhnsa
defbc5b95d Umbenennen des Projektordners 2025-03-15 21:21:53 +01:00
177 changed files with 468 additions and 13540 deletions

5
.gitignore vendored
View File

@@ -1,10 +1,5 @@
__pycache__ __pycache__
.venv .venv
.env
db.sqlite3 db.sqlite3
tailwindcss.exe tailwindcss.exe
t-style.css t-style.css
dump.rdb
media
node_modules
staticfiles

View File

@@ -7,7 +7,6 @@
3. [Superuser erstellen](#superuser-erstellen) 3. [Superuser erstellen](#superuser-erstellen)
4. [Tailwind-Nutzung](#tailwind-nutzung) 4. [Tailwind-Nutzung](#tailwind-nutzung)
5. [Issue - Merge Request - Merge](#issue-mergerequest-merge) 5. [Issue - Merge Request - Merge](#issue-mergerequest-merge)
6. [Konfiguration](#konfiguration)
## Initialisierung des Projekts ## Initialisierung des Projekts
@@ -41,69 +40,23 @@ Um das Projekt erfolgreich zu starten, folge diesen Schritten:
``` ```
7. Die Datenbank migrieren: 7. Die Datenbank migrieren:
```sh ```sh
python django/manage.py migrate python3 core/manage.py migrate
``` ```
## Starten des Servers ## Starten des Servers
### Entwicklungsserver starten ### Entwicklungsserver starten
Für die Entwicklung mit WebSocket-Unterstützung verwenden wir Daphne: Um den Server zu starten, verwende einen der folgenden Befehle:
1. Aktiviere die virtuelle Umgebung: - Standardport (8000):
```sh ```sh
source .venv/bin/activate # Linux python3 core/manage.py runserver
.venv\Scripts\activate # Windows ```
``` - Bestimmten Port (z. B. 9000):
```sh
2. Starte den Daphne-Server: python3 core/manage.py runserver 9000
```sh ```
# Im Verzeichnis 'django'
daphne -b 127.0.0.1 -p 8000 core.asgi:application
```
3. Starte den REDIS Server
```sh
redis-server
```
> **Hinweis:** Der Redis Server muss, wenn er nicht auf dem selben Gerät unter Standardeinstellungen läuft, in der Datei play/consumers/lobby.py und core/settings.py entsprechend umkonfiguriert werden!
Alternativ kannst du den Django-Entwicklungsserver verwenden, wenn du keine WebSocket-Funktionalität benötigst:
```sh
python3 core/manage.py runserver
```
### Produktionsserver starten
Für den Produktivbetrieb verwenden wir Daphne als ASGI-Server, der sowohl HTTP als auch WebSocket-Verbindungen unterstützt:
1. Aktiviere die virtuelle Umgebung wie oben beschrieben
2. Sammle die statischen Dateien:
```sh
python3 core/manage.py collectstatic
```
3. Starte den Daphne-Server:
```sh
daphne -b 0.0.0.0 -p 8000 core.asgi:application
```
- `-b 0.0.0.0`: Bindet den Server an alle Netzwerk-Interfaces
- `-p 8000`: Port (anpassbar)
4. Cronjob für Bereinigung der Spiele in der Datenbank
- Zum bearbeiten der Cronjobs:
```sh
crontab -e
```
- Dann `0 0 * * * /path/to/venv/bin/python /path/to/your_project/manage.py cleanup_games` einfügen, damit um Mitternacht alle Spiele gelöscht werden
5. Starte den REDIS Server
```sh
redis-server
```
> **Hinweis:** Der Redis Server muss, wenn er nicht auf dem selben Gerät unter Standardeinstellungen läuft, in der Datei play/consumers/lobby.py und core/settings.py entsprechend umkonfiguriert werden!
Damit ist das Projekt erfolgreich eingerichtet und der Server kann gestartet werden! Damit ist das Projekt erfolgreich eingerichtet und der Server kann gestartet werden!
@@ -147,10 +100,10 @@ npx @tailwindcss/cli -i <INPUT-DATEI> -o <OUTPUT-DATEI> --watch --minify
```sh ```sh
# unter Linux mit Tailwind Binärdatei: # unter Linux mit Tailwind Binärdatei:
npx tailwindcss -i static/css/t-input.css -o static/css/t-style.css --watch --minify tailwindcss -i core/static/homepage/t-input.css -o core/static/homepage/t-style.css --watch --minify
# unter Windows mit NPM (in bash mit '/' statt '\'): # unter Windows mit NPM:
npx @tailwindcss/cli -i ./static/css/t-input.css -o ./static/css/t-style.css --watch --minify npx @tailwindcss/cli -i .\core\static\homepage\t-input.css -o .\core\static\homepage\t-style.css --watch --minify
``` ```
## Issue - Merge Request - Merge ## Issue - Merge Request - Merge
@@ -159,23 +112,6 @@ Hier der vereinbarte Arbeitsablauf:
1. Issue erstellen - das geht am einfachsten über die Webseite. Der Titel beschreibt kurz und klar, was angestrebt wird. 1. Issue erstellen - das geht am einfachsten über die Webseite. Der Titel beschreibt kurz und klar, was angestrebt wird.
2. Merge Request anlegen - dadurch wird der Branch automatisch angelegt. Dieser kann mit `git pull` geholt und mit `git checkout <branch>` bearbeitet werden. 2. Merge Request anlegen - dadurch wird der Bransch automatisch angelegt. Dieser kann mit `git pull` geholt und mit `git checkout <branch>` bearbeitet werden.
3. Nach dem Hochladen kann der Merge in den Master durchgeführt werden. 3. Nach dem Hochladen kann der Merge in den Master durchgeführt werden.
## Konfiguration
Die Konfiguration des Projekts befindet sich im `config/qivip_config.json`.
| Variable | Bedeutung | Optionen |
| --- | --- | --- |
| ENABLE_RATING_SYSTEM | Soll das Bewertungssystem aktiviert werden? | true [Standard], false |
## 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

View File

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

View File

@@ -1,7 +0,0 @@
Name und Anschrift des Verantwortlichen
//NAME HIER EINSETZEN//
//ADRESSE HIER EINSETZEN//
//E-MAIL HIER EINSETZEN//

Binary file not shown.

View File

@@ -6,15 +6,11 @@ from django import forms
class RegisterForm(UserCreationForm): class RegisterForm(UserCreationForm):
class Meta: class Meta:
model=User model=User
fields = ['username','password1','password2', "email"] fields = ['username','password1','password2']
username = forms.CharField( username = forms.CharField(
widget=forms.TextInput(attrs={'placeholder': 'BENUTZERNAME'}) widget=forms.TextInput(attrs={'placeholder': 'BENUTZERNAME'})
) )
email = forms.EmailField(
widget=forms.TextInput(attrs={'placeholder': 'E-MAIL'})
)
password1 = forms.CharField( password1 = forms.CharField(
widget=forms.PasswordInput(attrs={'placeholder': 'PASSWORT'}) widget=forms.PasswordInput(attrs={'placeholder': 'PASSWORT'})
) )
@@ -23,3 +19,4 @@ class RegisterForm(UserCreationForm):
) )

View File

@@ -1,8 +1,7 @@
from django.urls import path, reverse_lazy # type: ignore from django.urls import path # type: ignore
from . import views from . import views
from django.contrib.auth.views import LogoutView from django.contrib.auth.views import LogoutView
from django.contrib.auth.views import LoginView from django.contrib.auth.views import LoginView
from django.contrib.auth import views as auth_views
app_name = 'accounts' # Namensraum zum verhindern von Konflikten zwischen Apps app_name = 'accounts' # Namensraum zum verhindern von Konflikten zwischen Apps
# #
@@ -14,22 +13,4 @@ urlpatterns = [
path('logout/', LogoutView.as_view(next_page='accounts:login'), name='logout'), path('logout/', LogoutView.as_view(next_page='accounts:login'), name='logout'),
path('login/', LoginView.as_view(template_name='accounts/login.html', next_page='accounts:home'), name='login'), path('login/', LoginView.as_view(template_name='accounts/login.html', next_page='accounts:home'), name='login'),
path('register/', views.register, name='register'), path('register/', views.register, name='register'),
path('password_reset/', auth_views.PasswordResetView.as_view(success_url=reverse_lazy('accounts:password_reset_done')), name='password_reset'),
path('password_reset/done/', auth_views.PasswordResetDoneView.as_view(), name='password_reset_done'),
path('reset/done/', auth_views.PasswordResetCompleteView.as_view(), name='password_reset_complete'),
path('password_change/', views.password_changed, name='password_change'),
path('password_change/done/', auth_views.PasswordChangeDoneView.as_view(), name='password_change_done'),
path('delete_account/', views.deleteAccount, name="delete_account"),
path(
'reset/<uidb64>/<token>/',
auth_views.PasswordResetConfirmView.as_view(
template_name='registration/password_reset_confirm.html',
success_url=reverse_lazy('accounts:password_reset_complete')
),
name='password_reset_confirm'
),
] ]

View File

@@ -1,71 +1,19 @@
from django.shortcuts import render, redirect from django.shortcuts import render, redirect
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth import login, authenticate
from .forms import RegisterForm from .forms import RegisterForm
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. # Create your views here.
@login_required @login_required
def home(request): def home(request):
own_critisized_quizzes = QivipQuiz.objects.annotate( return render(request, 'accounts/home.html')
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)
def register(response): def register(response):
if response.method == "POST": if response.method == "POST":
form = RegisterForm(response.POST) form = RegisterForm(response.POST)
if form.is_valid(): if form.is_valid():
user=form.save() form.save()
login(response, user) #automatischer Login nach Registrierung return redirect("home")
return redirect("accounts:home")
else: else:
form = RegisterForm() form = RegisterForm()
return render(response, "accounts/register.html", {"form":form}) return render(response, "accounts/register.html", {"form":form})
from django.contrib.auth import views as auth_views
@login_required
def password_changed(request):
if request.method == 'POST':
form = PasswordChangeForm(user=request.user, data=request.POST)
if form.is_valid():
user = form.save()
#update_session_auth_hash(request, user) #Hier wäre der User nach Passwortänderung noch angemeldet!
logout(request)
messages.success(request, "Das Passwort wurde erfolgreich geändert!")
return redirect('accounts:home')
else:
form = PasswordChangeForm(user=request.user)
return render(request, 'registration/password_change_form.html', {'form': form})
@login_required
def deleteAccount(request):
if request.method == 'POST':
user=request.user
logout(request)
user.delete()
messages.success(request, "Der Account wurde erfolgreich gelöscht!")
return redirect('homepage:home')
return render(request, 'registration/delete_account_confirm.html')

View File

@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class ComponentsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'components'

View File

View File

@@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

14
django/components/urls.py Normal file
View File

@@ -0,0 +1,14 @@
from django.urls import path # type: ignore
from . import views
app_name = 'components' # Namensraum zum verhindern von Konflikten zwischen Apps
#
# Wichtig: Damit Links funktionieren müssen diese so eingebunden werden: {% url 'accounts:login' %} statt {% url 'login' %}
#
urlpatterns = [
path('<int:pk>', views.play, name='play'),
path('ranking/<int:pk>', views.show_rank, name='ranking')
]

View File

@@ -0,0 +1,9 @@
from django.shortcuts import render
# Create your views here.
def play(request,pk): #
return render(request, 'components/answer.html', {"pk": pk}) #
def show_rank(request,pk): #
return render(request, 'components/ranking.html', {"pk": pk}) #

View File

@@ -1,3 +0,0 @@
{
"ENABLE_RATING_SYSTEM": true
}

View File

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

View File

@@ -10,60 +10,42 @@ For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.1/ref/settings/ https://docs.djangoproject.com/en/5.1/ref/settings/
""" """
import json, os
from pathlib import Path from pathlib import Path
from dotenv import load_dotenv
# Build paths inside the project like this: BASE_DIR / 'subdir'. # Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent BASE_DIR = Path(__file__).resolve().parent.parent
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' #TODO: NUR FÜR ENTWICKLUNG
# Quick-start development settings - unsuitable for production # Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/ # See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/
# Support env variables from .env file if defined
env_path = load_dotenv(os.path.join(BASE_DIR, '.env'))
load_dotenv(env_path)
# SECURITY WARNING: keep the secret key used in production secret! # SECURITY WARNING: keep the secret key used in production secret!
#SECRET_KEY = 'django-insecure-#+s307$rxkts=8@+_^i)8)n1b4*y4za1xz!mvv(h@!+n9!mp9)' SECRET_KEY = 'django-insecure-#+s307$rxkts=8@+_^i)8)n1b4*y4za1xz!mvv(h@!+n9!mp9)'
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! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True DEBUG = True
#DEBUG = False
#DEBUG = os.environ.get('DJANGO_DEBUG', '') != 'False'
ALLOWED_HOSTS = ['127.0.0.1', 'localhost', '*'] ALLOWED_HOSTS = ['127.0.0.1', 'localhost', '*']
# Konfigurationsdatei für Projekt
with open(BASE_DIR / 'config' / 'qivip_config.json') as config_file:
QIVIP_CONFIG = json.load(config_file)
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
'library.apps.LibraryConfig', 'library.apps.LibraryConfig',
'homepage.apps.HomepageConfig', 'homepage.apps.HomepageConfig',
'components.apps.ComponentsConfig',
'accounts.apps.AccountsConfig', 'accounts.apps.AccountsConfig',
'play', 'play.apps.PlayConfig',
'django.contrib.admin', 'django.contrib.admin',
'django.contrib.auth', 'django.contrib.auth',
'django.contrib.contenttypes', 'django.contrib.contenttypes',
'django.contrib.sessions', 'django.contrib.sessions',
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'channels',
'django_cleanup',
'django.contrib.humanize',
'dal',
'dal_select2',
] ]
MIDDLEWARE = [ MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', 'django.middleware.csrf.CsrfViewMiddleware',
@@ -91,17 +73,6 @@ TEMPLATES = [
] ]
WSGI_APPLICATION = 'core.wsgi.application' WSGI_APPLICATION = 'core.wsgi.application'
ASGI_APPLICATION = 'core.asgi.application'
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [("127.0.0.1", 6380)],
},
},
}
# Database # Database
@@ -114,15 +85,6 @@ DATABASES = {
} }
} }
STORAGES = {
"staticfiles": {
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
},
"default": {
"BACKEND": "django.core.files.storage.FileSystemStorage",
},
}
# Password validation # Password validation
# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
@@ -159,32 +121,11 @@ USE_TZ = True
# https://docs.djangoproject.com/en/5.1/howto/static-files/ # https://docs.djangoproject.com/en/5.1/howto/static-files/
STATIC_URL = '/static/' STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / "staticfiles"
STATICFILES_DIRS = [BASE_DIR / 'static'] STATICFILES_DIRS = [BASE_DIR / 'static']
# WhiteNoise configuration
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
WHITENOISE_SKIP_COMPRESS_EXTENSIONS = ['css', 'js']
# Return 404 instead of 500 for missing files
WHITENOISE_MISSING_FILE_ERRNO = None
# Default primary key field type # Default primary key field type
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field # https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
LOGIN_URL = '/account/login/' LOGIN_URL = '/account/login/'
import os
MEDIA_URL = '/media/' # URL, um auf Medien-Dateien zuzugreifen
MEDIA_ROOT = os.path.join(BASE_DIR, 'media') # Speicherort für hochgeladene Dateien
# 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'

View File

@@ -14,8 +14,6 @@ Including another URLconf
1. Import the include() function: from django.urls import include, path 1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
""" """
from django.conf.urls.static import static
from django.conf import settings
from django.contrib import admin # type: ignore from django.contrib import admin # type: ignore
from django.urls import path, include from django.urls import path, include
@@ -26,6 +24,3 @@ urlpatterns = [
path('play/', include('play.urls')), path('play/', include('play.urls')),
path('library/', include('library.urls')), path('library/', include('library.urls')),
] ]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View File

@@ -1,12 +1,3 @@
from django.contrib import admin from django.contrib import admin
from .models import QivipFaq, QivipDescription
# Register your models here. # 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)

View File

@@ -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)),
],
),
]

View File

@@ -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',
),
]

View File

@@ -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),
),
]

View File

@@ -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)),
],
),
]

View File

@@ -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',
),
]

View File

@@ -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',
),
]

View File

@@ -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),
),
]

View File

@@ -1,10 +1,3 @@
from django.db import models from django.db import models
# Create your models here. # 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")

View File

@@ -1,11 +1,7 @@
from django.shortcuts import render, redirect from django.shortcuts import render, redirect
from .models import QivipFaq, QivipDescription
def home(request): def home(request):
faqs=QivipFaq.objects.all() return render(request, 'homepage/home.html')
descriptions=QivipDescription.objects.all()
context={"faqs": faqs, "descriptions": descriptions}
return render(request, 'homepage/home.html', context)
def impress(request): def impress(request):
return render(request, 'homepage/impress.html') return render(request, 'homepage/impress.html')

View File

@@ -1,25 +1,15 @@
from django.contrib import admin from django.contrib import admin
from .models import QivipQuiz, QivipQuestion,QivipQuizFavorite, QuestionImage, QuizCategory, QuizImage, QuizRating, QuizCriticism from .models import QivipQuiz, QivipQuestion, QuizCategory, Tag
# Für das QivipQuiz Modell # Für das QivipQuiz Modell
class QivipQuizAdmin(admin.ModelAdmin): class QivipQuizAdmin(admin.ModelAdmin):
list_display = ('name','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 search_fields = ('name', 'user_id__username', 'category__name') # Suchfelder
list_filter = ('status', 'category') # Filteroptionen 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 # Für das QivipQuestion Modell
class QivipQuestionAdmin(admin.ModelAdmin): class QivipQuestionAdmin(admin.ModelAdmin):
list_display = ('id', 'quiz_id', 'data', 'creation_date', 'update_date', 'credits') list_display = ('quiz_id', 'data', 'creation_date', 'update_date')
search_fields = ('data',) search_fields = ('data',)
list_filter = ('quiz_id',) list_filter = ('quiz_id',)
@@ -29,47 +19,13 @@ class QuizCategoryAdmin(admin.ModelAdmin):
search_fields = ('name',) search_fields = ('name',)
prepopulated_fields = {'slug': ('name',)} # Erstelle automatisch den Slug basierend auf dem Namen prepopulated_fields = {'slug': ('name',)} # Erstelle automatisch den Slug basierend auf dem Namen
# Für das Tag Modell
class TagAdmin(admin.ModelAdmin):
class QivipQuizFavoriteAdmin(admin.ModelAdmin): list_display = ('name',) # Zeige nur den Namen des Tags
list_display = ('user', 'favorite', 'quiz') # Welche Felder sollen in der Übersicht angezeigt werden search_fields = ('name',)
class QuizImageAdmin(admin.ModelAdmin):
list_display = ('image',)
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 # Registrierung der Modelle im Admin
admin.site.register(QivipQuiz, QivipQuizAdmin) admin.site.register(QivipQuiz, QivipQuizAdmin)
admin.site.register(QivipQuestion, QivipQuestionAdmin) admin.site.register(QivipQuestion, QivipQuestionAdmin)
admin.site.register(QuizCriticism, QuizCriticismAdmin) admin.site.register(QuizCategory, QuizCategoryAdmin)
admin.site.register(QivipQuizFavorite, QivipQuizFavoriteAdmin) admin.site.register(Tag, TagAdmin)
admin.site.register(QuizImage, QuizImageAdmin)
admin.site.register(QuestionImage, QuestionImageAdmin)

View File

@@ -1,61 +1,12 @@
from django import forms from django import forms
from .models import QivipQuiz, QivipQuestion, QuizCategory from .models import QivipQuiz, QivipQuestion
from dal import autocomplete
class QuizForm(forms.ModelForm): class QuizForm(forms.ModelForm):
class Meta: class Meta:
model = QivipQuiz model = QivipQuiz
fields = ['name', 'description', 'status', 'category','difficulty','reference_quizzes','credits'] fields = ['name', 'description', 'status', 'category', 'tags',"difficulty"]
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})"
class QuestionForm(forms.ModelForm): class QuestionForm(forms.ModelForm):
class Meta: class Meta:
model = QivipQuestion model = QivipQuestion
fields = ['quiz_id', 'data','time_per_question'] fields = ['quiz_id', 'data']
from django import forms
class QuizFilterForm(forms.Form):
search = forms.CharField(required=False, label="Suche", widget=forms.TextInput(attrs={
'class': 'flex flex-grow rounded-lg p-2' ,'placeholder': 'Suche ...', 'list': 'quiz-names',
}))
min_amount_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={
'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'
}))

View File

@@ -1,13 +0,0 @@
from django.core.management.base import BaseCommand
from play.models import QuizGame
from django.utils import timezone
class Command(BaseCommand):
help = 'Bereinigt beendete Spiele'
def handle(self, *args, **kwargs):
expired_games = QuizGame.objects.filter(
updated_at__lt=timezone.now() - timezone.timedelta(minutes=5))
deleted_count = expired_games.count()
expired_games.delete()
self.stdout.write(f'{deleted_count} alte Spiele gelöscht.')

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.7 on 2025-03-16 12:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0015_alter_qivipquiz_difficulty'),
]
operations = [
migrations.AlterField(
model_name='qivipquiz',
name='status',
field=models.CharField(choices=[('öffentlich', 'Öffentlich'), ('versteckt', 'Versteckt'), ('privat', 'Privat')], default='öffentlich', max_length=10),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.7 on 2025-03-16 12:54
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0016_alter_qivipquiz_status'),
]
operations = [
migrations.AlterField(
model_name='qivipquiz',
name='difficulty',
field=models.CharField(choices=[('1', '1'), ('2', '2'), ('3', '3'), ('4', '4'), ('5', '5'), ('nicht gesetzt', 'nicht gesetzt')], default='nicht gesetzt', help_text='1: niedrigste Schwierigkeit und 5: höchste Schwierigkeit', max_length=13),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.7 on 2025-03-16 13:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0017_alter_qivipquiz_difficulty'),
]
operations = [
migrations.AlterField(
model_name='qivipquiz',
name='description',
field=models.TextField(blank=True, default='In dem Quiz geht es um ...', max_length=150, null=True),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.7 on 2025-03-16 13:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0018_alter_qivipquiz_description'),
]
operations = [
migrations.AlterField(
model_name='qivipquiz',
name='description',
field=models.TextField(default='In dem Quiz geht es um ...', max_length=255, null=True),
),
]

View File

@@ -1,19 +0,0 @@
# Generated by Django 5.1.7 on 2025-03-16 13:08
import library.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0019_alter_qivipquiz_description'),
]
operations = [
migrations.AlterField(
model_name='qivipquiz',
name='description',
field=models.TextField(default='In dem Quiz geht es um ...', max_length=255, validators=[library.models.QivipQuiz.validate_description]),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.7 on 2025-03-16 14:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0020_alter_qivipquiz_description'),
]
operations = [
migrations.AlterField(
model_name='qivipquiz',
name='name',
field=models.CharField(max_length=100),
),
]

View File

@@ -1,24 +0,0 @@
# Generated by Django 5.1.7 on 2025-03-16 14:35
import library.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0021_alter_qivipquiz_name'),
]
operations = [
migrations.AlterField(
model_name='qivipquiz',
name='description',
field=models.TextField(default='In dem Quiz geht es um ...', max_length=200, validators=[library.models.QivipQuiz.validate_description]),
),
migrations.AlterField(
model_name='qivipquiz',
name='name',
field=models.CharField(max_length=75),
),
]

View File

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

View File

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

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-03 16:46
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0024_alter_qivipquiz_image'),
]
operations = [
migrations.AddField(
model_name='qivipquiz',
name='modified_description',
field=models.TextField(blank=True, default='Das Quiz wurde modifiziert von:', editable=False),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-04 13:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0025_qivipquiz_modified_description'),
]
operations = [
migrations.AlterField(
model_name='qivipquiz',
name='modified_description',
field=models.TextField(blank=True, editable=False),
),
]

View File

@@ -1,21 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-04 13:37
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0026_alter_qivipquiz_modified_description'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='qivipquiz',
name='creator',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='creator_quiz', to=settings.AUTH_USER_MODEL),
),
]

View File

@@ -1,21 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-04 13:57
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0027_qivipquiz_creator'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterField(
model_name='qivipquiz',
name='creator',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='creator_quiz', to=settings.AUTH_USER_MODEL),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-04 16:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0028_alter_qivipquiz_creator'),
]
operations = [
migrations.AddField(
model_name='qivipquiz',
name='credits',
field=models.TextField(blank=True),
),
]

View File

@@ -1,27 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-05 09:39
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0029_qivipquiz_credits'),
]
operations = [
migrations.RemoveField(
model_name='qivipquiz',
name='creator',
),
migrations.RemoveField(
model_name='qivipquiz',
name='modified_description',
),
migrations.AddField(
model_name='qivipquiz',
name='base_quiz',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='child_quizzes', to='library.qivipquiz'),
),
]

View File

@@ -1,19 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-05 17:45
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0030_remove_qivipquiz_creator_and_more'),
]
operations = [
migrations.AlterField(
model_name='qivipquestion',
name='quiz_id',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='questions', to='library.qivipquiz'),
),
]

View File

@@ -1,37 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-05 19:08
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0031_alter_qivipquestion_quiz_id'),
]
operations = [
migrations.AddField(
model_name='qivipquiz',
name='average_rating',
field=models.FloatField(default=3.0),
),
migrations.AddField(
model_name='qivipquiz',
name='rating_count',
field=models.IntegerField(default=0),
),
migrations.CreateModel(
name='QuizRating',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('participant_id', models.CharField(max_length=200)),
('rating', models.IntegerField(choices=[(1, '1'), (2, '2'), (3, '3'), (4, '4'), (5, '5')])),
('created_at', models.DateTimeField(auto_now_add=True)),
('quiz', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ratings', to='library.qivipquiz')),
],
options={
'unique_together': {('quiz', 'participant_id')},
},
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-08 14:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0032_qivipquiz_average_rating_qivipquiz_rating_count_and_more'),
]
operations = [
migrations.AddField(
model_name='qivipquestion',
name='time_per_question',
field=models.CharField(choices=[('10', 10), ('15', 15), ('30', 30), ('60', 60), ('90', 90), ('120', 120), ('180', 180), ('nicht gesetzt', 'nicht gesetzt')], default=30, help_text='Zeit pro Frage in Sekunden', max_length=13),
),
]

View File

@@ -1,20 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-10 13:59
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('library', '0032_qivipquiz_average_rating_qivipquiz_rating_count_and_more'),
]
operations = [
migrations.RemoveField(
model_name='qivipquiz',
name='tags',
),
migrations.DeleteModel(
name='Tag',
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-08 14:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0033_qivipquestion_time_per_question'),
]
operations = [
migrations.AlterField(
model_name='qivipquestion',
name='time_per_question',
field=models.CharField(choices=[('10', 10), ('15', 15), ('30', 30), ('60', 60), ('90', 90), ('120', 120), ('180', 180)], default=30, help_text='Zeit pro Frage in Sekunden', max_length=13),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-10 14:04
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0033_remove_qivipquiz_tags_delete_tag'),
]
operations = [
migrations.AlterField(
model_name='qivipquiz',
name='status',
field=models.CharField(choices=[('öffentlich', 'Öffentlich'), ('privat', 'Privat')], default='öffentlich', max_length=10),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-08 14:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0034_alter_qivipquestion_time_per_question'),
]
operations = [
migrations.AlterField(
model_name='qivipquestion',
name='time_per_question',
field=models.CharField(choices=[('10', 10), ('15', 15), ('30', 30), ('60', 60), ('90', 90), ('120', 120), ('180', 180)], default=30, help_text='Zeit pro Frage in Sekunden', max_length=3),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-10 14:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0034_alter_qivipquiz_status'),
]
operations = [
migrations.AddField(
model_name='qivipquiz',
name='favorite',
field=models.BooleanField(default=False),
),
]

View File

@@ -1,32 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-10 16:02
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0035_qivipquiz_favorite'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.RemoveField(
model_name='qivipquiz',
name='favorite',
),
migrations.CreateModel(
name='QuizFavorite',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('favorite', models.BooleanField(default=False)),
('quiz', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='library.qivipquiz')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'unique_together': {('user', 'quiz')},
},
),
]

View File

@@ -1,19 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-10 16:05
from django.conf import settings
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('library', '0036_remove_qivipquiz_favorite_quizfavorite'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.RenameModel(
old_name='QuizFavorite',
new_name='QivipQuizFavorite',
),
]

View File

@@ -1,19 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-10 17:08
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0037_rename_quizfavorite_qivipquizfavorite'),
]
operations = [
migrations.AlterField(
model_name='qivipquizfavorite',
name='quiz',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='favorites', to='library.qivipquiz'),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-11 12:40
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0038_alter_qivipquizfavorite_quiz'),
]
operations = [
migrations.AddField(
model_name='qivipquizfavorite',
name='favorite_date',
field=models.DateTimeField(auto_now=True),
),
]

View File

@@ -1,19 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-11 13:15
import library.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0039_qivipquizfavorite_favorite_date'),
]
operations = [
migrations.AlterField(
model_name='qivipquiz',
name='description',
field=models.TextField(default='In dem Quiz geht es um ...', max_length=200, validators=[library.models.QivipQuiz.validate_description], verbose_name='Beschreibung'),
),
]

View File

@@ -1,34 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-11 13:21
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0040_alter_qivipquiz_description'),
]
operations = [
migrations.AlterField(
model_name='qivipquiz',
name='category',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='quiz', to='library.quizcategory', verbose_name='Kategorie'),
),
migrations.AlterField(
model_name='qivipquiz',
name='difficulty',
field=models.CharField(choices=[('1', '1'), ('2', '2'), ('3', '3'), ('4', '4'), ('5', '5'), ('nicht gesetzt', 'nicht gesetzt')], default='nicht gesetzt', help_text='1: niedrigste Schwierigkeit und 5: höchste Schwierigkeit', max_length=13, verbose_name='Schwierigkeit'),
),
migrations.AlterField(
model_name='qivipquiz',
name='image',
field=models.ImageField(blank=True, max_length=256, null=True, upload_to='quiz_images/', verbose_name='Bild'),
),
migrations.AlterField(
model_name='qivipquiz',
name='name',
field=models.CharField(max_length=75, verbose_name='Name des Quizzes'),
),
]

View File

@@ -1,19 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-11 13:43
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0041_alter_qivipquiz_category_alter_qivipquiz_difficulty_and_more'),
]
operations = [
migrations.AddField(
model_name='qivipquiz',
name='favorite',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='quizzes', to='library.qivipquizfavorite'),
),
]

View File

@@ -1,17 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-11 15:25
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('library', '0042_qivipquiz_favorite'),
]
operations = [
migrations.RemoveField(
model_name='qivipquiz',
name='favorite',
),
]

View File

@@ -1,22 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-11 16:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0043_remove_qivipquiz_favorite'),
]
operations = [
migrations.RemoveField(
model_name='qivipquizfavorite',
name='favorite_date',
),
migrations.AddField(
model_name='qivipquiz',
name='favorite_date',
field=models.DateTimeField(auto_now=True),
),
]

View File

@@ -1,22 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-11 16:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0044_remove_qivipquizfavorite_favorite_date_and_more'),
]
operations = [
migrations.RemoveField(
model_name='qivipquiz',
name='favorite_date',
),
migrations.AddField(
model_name='qivipquizfavorite',
name='favorite_date',
field=models.DateTimeField(auto_now=True),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-11 16:50
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0045_remove_qivipquiz_favorite_date_and_more'),
]
operations = [
migrations.AlterField(
model_name='qivipquizfavorite',
name='favorite_date',
field=models.DateTimeField(blank=True, null=True),
),
]

View File

@@ -1,14 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-18 11:34
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('library', '0035_alter_qivipquestion_time_per_question'),
('library', '0046_alter_qivipquizfavorite_favorite_date'),
]
operations = [
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-18 11:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0047_merge_20250418_1334'),
]
operations = [
migrations.AddField(
model_name='qivipquestion',
name='image',
field=models.ImageField(blank=True, height_field=900, max_length=200, null=True, upload_to='question_images/', verbose_name='Bild', width_field=1000),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-18 19:04
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0048_qivipquestion_image'),
]
operations = [
migrations.AlterField(
model_name='qivipquestion',
name='image',
field=models.ImageField(blank=True, null=True, upload_to='question_images/', verbose_name='Bild'),
),
]

View File

@@ -1,38 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-22 16:04
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0049_alter_qivipquestion_image'),
]
operations = [
migrations.CreateModel(
name='QuestionImage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('image', models.ImageField(blank=True, null=True, upload_to='question_images/', verbose_name='Bild')),
],
),
migrations.CreateModel(
name='QuizImage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('image', models.ImageField(blank=True, max_length=256, null=True, upload_to='quiz_images/', verbose_name='Bild')),
],
),
migrations.AlterField(
model_name='qivipquestion',
name='image',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='questions', to='library.questionimage'),
),
migrations.AlterField(
model_name='qivipquiz',
name='image',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='quizzes', to='library.QuizImage'),
),
]

View File

@@ -1,24 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-23 12:04
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0050_questionimage_quizimages_alter_qivipquestion_image_and_more'),
]
operations = [
migrations.AlterField(
model_name='qivipquestion',
name='image',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='questions', to='library.questionimage'),
),
migrations.AlterField(
model_name='qivipquiz',
name='image',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='quizzes', to='library.quizimage'),
),
]

View File

@@ -1,29 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-23 12:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0051_alter_qivipquestion_image_alter_qivipquiz_image'),
]
operations = [
migrations.AlterField(
model_name='qivipquestion',
name='image',
field=models.ImageField(blank=True, null=True, upload_to='question_images/', verbose_name='Bild'),
),
migrations.AlterField(
model_name='qivipquiz',
name='image',
field=models.ImageField(blank=True, max_length=256, null=True, upload_to='quiz_images/', verbose_name='Bild'),
),
migrations.DeleteModel(
name='QuestionImage',
),
migrations.DeleteModel(
name='QuizImage',
),
]

View File

@@ -1,38 +0,0 @@
# Generated by Django 5.1.7 on 2025-04-23 13:15
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0052_alter_qivipquestion_image_alter_qivipquiz_image_and_more'),
]
operations = [
migrations.CreateModel(
name='QuestionImage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('image', models.ImageField(blank=True, null=True, upload_to='question_images/', verbose_name='Bild')),
],
),
migrations.CreateModel(
name='QuizImage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('image', models.ImageField(blank=True, max_length=256, null=True, upload_to='quiz_images/', verbose_name='Bild')),
],
),
migrations.AlterField(
model_name='qivipquestion',
name='image',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='questions', to='library.questionimage'),
),
migrations.AlterField(
model_name='qivipquiz',
name='image',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='quizzes', to='library.quizimage'),
),
]

View File

@@ -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),
),
]

View File

@@ -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),
),
]

View File

@@ -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(),
),
]

View File

@@ -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),
),
]

View File

@@ -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),
),
]

View File

@@ -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',
),
]

View File

@@ -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'),
),
]

View File

@@ -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'),
),
]

View File

@@ -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'),
),
]

View File

@@ -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',
),
]

View File

@@ -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')},
},
),
]

View File

@@ -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,
),
]

View File

@@ -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',
),
]

View File

@@ -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,
),
]

View File

@@ -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')},
),
]

View File

@@ -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),
),
]

View File

@@ -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'),
),
]

View File

@@ -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),
),
]

View File

@@ -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),
),
]

View File

@@ -2,22 +2,12 @@ from django.db import models
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.utils.text import slugify from django.utils.text import slugify
import uuid 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/
class QuestionImage(models.Model):
image = models.ImageField(verbose_name="Bild", upload_to="question_images/", blank=True, null=True)
class QivipQuiz(models.Model): class QivipQuiz(models.Model):
STATUS_VALUES = { STATUS_VALUES = {
"öffentlich": "Öffentlich", "public": "Öffentlich",
#"versteckt": "Versteckt", "hidden": "Versteckt",
"privat": "Privat", "private": "Privat",
} }
DIFFICULTY_VALUES = { DIFFICULTY_VALUES = {
"1": "1", "1": "1",
@@ -25,164 +15,46 @@ class QivipQuiz(models.Model):
"3": "3", "3": "3",
"4": "4", "4": "4",
"5": "5", "5": "5",
"nicht gesetzt":"nicht gesetzt", "not defined":"nicht gesetzt",
} }
def validate_description(value):
if value.strip() == "In dem Quiz geht es um ...":
raise ValidationError("Bitte gib eine aussagekräftige Beschreibung ein.")
image = models.ForeignKey(
QuizImage,
null=True,
blank=True,
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) uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
user_id = models.ForeignKey(User, on_delete=models.CASCADE, related_name='quiz') user_id = models.ForeignKey(User, on_delete=models.CASCADE, related_name='quiz')
base_quiz = models.ForeignKey('self', on_delete=models.SET_NULL,blank=True, null=True, related_name='child_quizzes')
creation_date = models.DateTimeField(auto_now_add=True) creation_date = models.DateTimeField(auto_now_add=True)
update_date = models.DateTimeField(auto_now=True) update_date = models.DateTimeField(auto_now=True)
status = models.CharField(max_length=10, choices=list(STATUS_VALUES.items()), default="öffentlich") status = models.CharField(max_length=10, choices=list(STATUS_VALUES.items()), default="public")
category = models.ForeignKey('QuizCategory',verbose_name="Kategorie", related_name='quiz', on_delete=models.CASCADE) category = models.ForeignKey('QuizCategory', related_name='quiz', on_delete=models.CASCADE)
name = models.CharField(max_length=75,verbose_name="Name des Quizzes") tags = models.ManyToManyField('Tag', blank=True)
description = models.TextField(validators=[validate_description],max_length=200,blank=False, default="In dem Quiz geht es um ...", verbose_name="Beschreibung") name = models.CharField(max_length=255)
difficulty= models.CharField(verbose_name="Schwierigkeit", max_length=13, choices=list(DIFFICULTY_VALUES.items()), default="nicht gesetzt", help_text="1: niedrigste Schwierigkeit und 5: höchste Schwierigkeit") description = models.TextField(blank=True, null=True, default="In dem Quiz geht es um ...")
credits=models.TextField(blank=True, editable=True) difficulty= models.CharField(max_length=11, choices=list(DIFFICULTY_VALUES.items()), default="not defined", help_text="1: niedrigste Schwierigkeit und 5: höchste Schwierigkeit")
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): def __str__(self):
return self.name return self.name
class QivipQuizFavorite(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE) # Verknüpfung zu User
quiz = models.ForeignKey(QivipQuiz, on_delete=models.CASCADE, related_name='favorites') # Verknüpfung zu Quiz
favorite = models.BooleanField(default=False) # Favoritenstatus
favorite_date = models.DateTimeField(null=True, blank=True)
class Meta:
unique_together = ('user', 'quiz') # Sicherstellen, dass es nur ein Favorit pro User und Quiz gibt
# Create your models here. # Create your models here.
class QivipQuestion(models.Model): class QivipQuestion(models.Model):
TIME_VALUES = {
"10": 10,
"15": 15,
"30": 30,
"60": 60,
"90": 90,
"120": 120,
"180": 180,
}
uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True) uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
quiz_id = models.ForeignKey(QivipQuiz, on_delete=models.CASCADE, related_name='questions') quiz_id = models.ForeignKey(QivipQuiz, on_delete=models.CASCADE, related_name='question')
creation_date = models.DateTimeField(auto_now_add=True) creation_date = models.DateTimeField(auto_now_add=True)
update_date = models.DateTimeField(auto_now=True) update_date = models.DateTimeField(auto_now=True)
data = models.TextField() data = models.TextField()
time_per_question = models.CharField(max_length=3, choices=list(TIME_VALUES.items()), default=30, help_text="Zeit pro Frage in Sekunden")
image = models.ForeignKey(
QuestionImage,
null=True,
blank=True,
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): def __str__(self):
return self.data[:50] return self.data[:50]
class QuizCriticism(models.Model): class Tag(models.Model):
quiz= models.ForeignKey(QivipQuiz, on_delete=models.CASCADE, related_name='criticism_quiz') name = models.CharField(max_length=100, unique=True)
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): def __str__(self):
return f"{self.question_criticism}" return self.name
class QuizRating(models.Model):
quiz = models.ForeignKey(QivipQuiz, on_delete=models.CASCADE, related_name='ratings')
participant_id = models.CharField(max_length=200)
rating = models.IntegerField(choices=[(i, str(i)) for i in range(1, 6)])
created_at = models.DateTimeField(auto_now_add=True)
#class Meta:
#unique_together = ('quiz', 'participant_id')
def save(self, *args, **kwargs):
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)
if is_new:
# Update quiz average rating
quiz = self.quiz
total_ratings = quiz.ratings.count()
avg_rating = quiz.ratings.aggregate(models.Avg('rating'))['rating__avg']
quiz.average_rating = round(avg_rating, 1) if avg_rating else 3.0
quiz.rating_count = total_ratings
quiz.save()"""
class QuizCategory(models.Model): class QuizCategory(models.Model):
name = models.CharField(max_length=100, unique=True) name = models.CharField(max_length=100, unique=True)
description = models.TextField(blank=True, null=True) description = models.TextField(blank=True, null=True)
slug = models.SlugField(unique=True, blank=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): def save(self, *args, **kwargs):
if not self.slug: if not self.slug:

View File

@@ -1,15 +1,10 @@
from django.urls import path from django.urls import path
from django.conf import settings
from . import views from . import views
from django.conf.urls.static import static
from .views import QuizAutocomplete
app_name = 'library' app_name = 'library'
urlpatterns = [ urlpatterns = [
path('', views.overview_quiz, name='overview_quiz'), 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('new/', views.new_quiz, name='new_quiz'),
path('edit/<int:pk>/', views.edit_quiz, name='edit_quiz'), path('edit/<int:pk>/', views.edit_quiz, name='edit_quiz'),
path('delete/<int:pk>/', views.delete_quiz, name='delete_quiz'), path('delete/<int:pk>/', views.delete_quiz, name='delete_quiz'),
@@ -17,18 +12,4 @@ urlpatterns = [
path('question/new/', views.new_question, name='new_question'), path('question/new/', views.new_question, name='new_question'),
path('question/edit/<int:pk>/', views.edit_question, name='edit_question'), path('question/edit/<int:pk>/', views.edit_question, name='edit_question'),
path('question/delete/<int:pk>/', views.delete_question, name='delete_question'), path('question/delete/<int:pk>/', views.delete_question, name='delete_question'),
path('detail/copy/<int:pk>/', views.copy_quiz, name='copy_quiz'),
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"
),
] ]

View File

@@ -1,414 +1,29 @@
from django.utils import timezone
import uuid
from django.http import HttpResponse, HttpResponseRedirect, JsonResponse
from django.shortcuts import render, redirect, get_object_or_404 from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.urls import reverse from django.urls import reverse
from django.contrib import messages from django.contrib import messages
from .models import QivipQuiz, QivipQuestion, QivipQuizFavorite, QuestionImage, QuizCriticism, QuizImage from .models import QivipQuiz, QivipQuestion
from .forms import QuizForm, QuestionForm from .forms import QuizForm, QuestionForm
import json import json
from django.core.paginator import Paginator
from .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 # Ü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)
if form.is_valid():
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)
if request.user.is_authenticated:
names = names | QivipQuiz.objects.filter(
user_id=request.user.id,
name__icontains=search
)
names=list(names.values_list('name', flat=True).distinct())
return JsonResponse(names, safe=False)
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)
def apply_quiz_filters(queryset, form):
if not form.is_valid():
return queryset
queryset = queryset.annotate(question_count=Count('questions'))
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')
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
)
# 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)
try:
my_quizzes = paginator_my.page(my_page)
except:
my_quizzes = paginator_my.page(1)
try:
all_quizzes = paginator_all.page(all_page)
except:
all_quizzes = paginator_all.page(1)
try:
favorite_quizzes = paginator_fav.page(fav_page)
except:
favorite_quizzes = paginator_fav.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/',
}
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")
@login_required @login_required
def favorite_quiz(request, pk): def overview_quiz(request):
quiz= get_object_or_404(QivipQuiz, pk=pk) quizzes = QivipQuiz.objects.filter(user_id=request.user)
favorite = QivipQuizFavorite.objects.filter(user=request.user, quiz=quiz).first() return render(request, 'library/overview_quiz.html', {'quizzes': quizzes})
if favorite:
favorite.favorite=not favorite.favorite
if favorite.favorite==True:
favorite.favorite_date = timezone.now()
favorite.save()
else:
favorite.favorite_date = None
favorite.delete()
statement="Das Quiz '"+ favorite.quiz.name+ "' wurde erfolgreich von den Favoriten entfernt!"
messages.error(request, statement)
else:
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'))
# Neues Quiz erstellen # Neues Quiz erstellen
@login_required @login_required
def new_quiz(request): def new_quiz(request):
if request.method == 'POST': if request.method == 'POST':
form = QuizForm(request.POST)
form = QuizForm(request.POST, request.FILES)
if form.is_valid(): if form.is_valid():
quiz = form.save(commit=False) 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
except:
pass
quiz.user_id = request.user quiz.user_id = request.user
#quiz.creator = request.user
quiz.save() quiz.save()
#form.save_m2m() # Speichert die Many-to-Many Beziehungen (Tags) form.save_m2m() # Speichert die Many-to-Many Beziehungen (Tags)
return redirect('library:detail_quiz', pk=quiz.pk) return redirect('library:edit_quiz', pk=quiz.pk)
else: else:
form = QuizForm() form = QuizForm()
return render(request, 'library/form.html', {'form': form}) return render(request, 'library/form.html', {'form': form})
@@ -418,43 +33,10 @@ def new_quiz(request):
def edit_quiz(request, pk): def edit_quiz(request, pk):
quiz = get_object_or_404(QivipQuiz, pk=pk, user_id=request.user) quiz = get_object_or_404(QivipQuiz, pk=pk, user_id=request.user)
if request.method == 'POST': if request.method == 'POST':
form = QuizForm(request.POST, instance=quiz, files=request.FILES) form = QuizForm(request.POST, instance=quiz)
if form.is_valid(): if form.is_valid():
if 'reset_image' in request.POST:
quiz.image = None
try:
quiz_image=request.FILES['quiz_image']
imagequiz=QuizImage.objects.create(image=quiz_image)
quiz.image=imagequiz
except:
pass
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() form.save()
for img in QuizImage.objects.all(): return redirect('library:overview_quiz')
try:
img.delete()
img.image.delete(save=False)
except:
pass
return redirect('library:detail_quiz', pk=pk)
#return modified(request, pk)
else: else:
form = QuizForm(instance=quiz) form = QuizForm(instance=quiz)
return render(request, 'library/form.html', {'form': form}) return render(request, 'library/form.html', {'form': form})
@@ -465,30 +47,15 @@ def delete_quiz(request, pk):
quiz = get_object_or_404(QivipQuiz, pk=pk, user_id=request.user) quiz = get_object_or_404(QivipQuiz, pk=pk, user_id=request.user)
if request.method == 'POST': if request.method == 'POST':
quiz.delete() quiz.delete()
for img in QuizImage.objects.all():
try:
img.delete()
img.image.delete(save=False)
except:
pass
for img in QuestionImage.objects.all():
try:
img.delete()
img.image.delete(save=False)
except:
pass
return redirect('library:overview_quiz') return redirect('library:overview_quiz')
return render(request, 'library/delete_confirmation.html', {'object': quiz}) return render(request, 'library/delete_confirmation.html', {'object': quiz})
# Quiz anzeigen # Quiz anzeigen
@login_required
def detail_quiz(request, pk): def detail_quiz(request, pk):
critisize_quiz(request, pk) quiz = get_object_or_404(QivipQuiz, pk=pk, user_id=request.user)
quiz = get_object_or_404(QivipQuiz, pk=pk)
show_answers = request.GET.get('show_answers', 'false').lower() == 'true'
questions = QivipQuestion.objects.filter(quiz_id=quiz) questions = QivipQuestion.objects.filter(quiz_id=quiz)
# Parse JSON data for each question # Parse JSON data for each question
for question in questions: for question in questions:
if question.data: if question.data:
@@ -496,57 +63,9 @@ def detail_quiz(request, pk):
context = { context = {
'quiz': quiz, 'quiz': quiz,
'questions': questions, 'questions': questions
'detail':show_answers,
} }
if quiz.status!="privat" or quiz.user_id==request.user: return render(request, 'library/detail_quiz.html', context)
return render(request, 'library/detail_quiz.html', context)
else:
return redirect('library:overview_quiz')
@login_required
def copy_quiz(request, pk):
original_quiz = get_object_or_404(QivipQuiz, pk=pk)
# Quiz kopieren (ohne ID, damit ein neues Objekt erstellt wird)
# Quiz kopieren (ohne ID, damit ein neues Objekt erstellt wird)
new_quiz = original_quiz # Kopie erstellen (aber Achtung: Noch gleiche Referenz!)
new_quiz.pk = None # Setzt die ID auf None, damit Django es als neues Objekt erkennt
new_quiz.uuid = uuid.uuid4() # Neue UUID generieren
new_quiz.user_id = request.user # Neuer Besitzer ist der aktuelle User
new_quiz.name += " - Kopie"
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
questions = QivipQuestion.objects.filter(quiz_id=pk)
for question in questions:
question.uuid = uuid.uuid4()
question.pk = None # Setze pk auf None, damit es als neues Objekt gespeichert wird
question.quiz_id = original_quiz # Verknüpfe mit dem neuen Quiz
question.data = question.data # `data`-Feld wird explizit übernommen # Verknüpfe mit dem neuen Quiz
question.save()
messages.success(request, "Quiz wurde erfolgreich kopiert!")
return redirect('library:detail_quiz', pk=new_quiz.pk)
# Übersicht aller Fragen # Übersicht aller Fragen
@login_required @login_required
@@ -565,7 +84,6 @@ def question_list(request):
def new_question(request): def new_question(request):
question_type = request.GET.get('type') question_type = request.GET.get('type')
quiz_id = request.GET.get('quiz_id') quiz_id = request.GET.get('quiz_id')
question_data = None
if not quiz_id: if not quiz_id:
messages.error(request, 'Quiz ID muss angegeben werden.') messages.error(request, 'Quiz ID muss angegeben werden.')
@@ -577,12 +95,12 @@ def new_question(request):
messages.error(request, 'Quiz nicht gefunden oder keine Berechtigung.') messages.error(request, 'Quiz nicht gefunden oder keine Berechtigung.')
return redirect('library:overview_quiz') 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']:
base_url = reverse('library:new_question') base_url = reverse('library:new_question')
return redirect(f'{base_url}?type=multiple_choice&quiz_id={quiz_id}') return redirect(f'{base_url}?type=multiple_choice&quiz_id={quiz_id}')
if request.method == 'POST': if request.method == 'POST':
question_text = request.POST.get('question', 'Kein Fragentext angegeben') question_text = request.POST.get('question', '')
# Handle different question types # Handle different question types
if question_type == 'true_false': if question_type == 'true_false':
@@ -595,18 +113,7 @@ def new_question(request):
{'value': 'Falsch', 'is_correct': not correct_answer} {'value': 'Falsch', 'is_correct': not correct_answer}
] ]
} }
elif question_type == 'multiple_choice':
elif question_type == 'input':
correct_answer = request.POST.get('correct_answer')
json_data = {
'type': question_type,
'question': question_text,
'options': [
{'value': correct_answer, 'is_correct': True}
]
}
elif question_type == 'multiple_choice' :
options = [] options = []
i = 1 i = 1
# Limit to maximum 6 options # Limit to maximum 6 options
@@ -630,77 +137,10 @@ def new_question(request):
'options': options '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 = QivipQuestion()
question.data = json.dumps(json_data) question.data = json.dumps(json_data)
question.quiz_id = quiz question.quiz_id = quiz
try:
question_image=request.FILES['question_image']
imagequestion=QuestionImage.objects.create(image=question_image)
question.image=imagequestion
except:
pass
question.time_per_question = request.POST.get('time_per_question', '30')
try:
question.credits=request.POST.get('credits',"")
except:
question.credits=""
question.save() question.save()
#return modified(request, pk=quiz.pk)
return redirect('library:detail_quiz', pk=quiz.pk) return redirect('library:detail_quiz', pk=quiz.pk)
else: else:
# Initialize empty question data for new questions # Initialize empty question data for new questions
@@ -713,19 +153,6 @@ def new_question(request):
{'value': 'Falsch', 'is_correct': False} {'value': 'Falsch', 'is_correct': False}
] ]
} }
standard_time_per_question = 30
elif question_type == 'input':
question_data = {
'type': question_type,
'question': '',
'options': [
]
}
standard_time_per_question = 30
elif question_type == 'multiple_choice': elif question_type == 'multiple_choice':
question_data = { question_data = {
'type': question_type, 'type': question_type,
@@ -735,51 +162,12 @@ def new_question(request):
{'value': '', 'is_correct': False} {'value': '', 'is_correct': False}
] ]
} }
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' template_name = f'library/question/question_{question_type}.html'
context = { context = {
'question': question_data, 'question': question_data,
'quiz_id': quiz_id, 'quiz_id': quiz_id,
'quiz': quiz, 'quiz': quiz
'standard_time_per_question': standard_time_per_question,
} }
return render(request, template_name, context) return render(request, template_name, context)
@@ -788,8 +176,6 @@ def new_question(request):
@login_required @login_required
def edit_question(request, pk): def edit_question(request, pk):
question = get_object_or_404(QivipQuestion, pk=pk, quiz_id__user_id=request.user) question = get_object_or_404(QivipQuestion, pk=pk, quiz_id__user_id=request.user)
if 'reset_ques_image' in request.POST:
question.image = None
if question.quiz_id.user_id != request.user: if question.quiz_id.user_id != request.user:
return redirect('library:question_list') return redirect('library:question_list')
@@ -797,7 +183,6 @@ def edit_question(request, pk):
try: try:
question_data = json.loads(question.data) if question.data else {} question_data = json.loads(question.data) if question.data else {}
question_type = question_data.get('type', 'multiple_choice') question_type = question_data.get('type', 'multiple_choice')
question_image = question.image
# For true/false questions, get the correct answer from the options # For true/false questions, get the correct answer from the options
if question_type == 'true_false': if question_type == 'true_false':
@@ -814,13 +199,6 @@ def edit_question(request, pk):
{'value': 'Falsch', 'is_correct': not correct_answer} {'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: else:
# For multiple choice questions # For multiple choice questions
question_data.setdefault('type', question_type) question_data.setdefault('type', question_type)
@@ -849,12 +227,6 @@ def edit_question(request, pk):
if request.method == 'POST': if request.method == 'POST':
question_text = request.POST.get('question', '') question_text = request.POST.get('question', '')
try:
question_image=request.FILES['question_image']
imagequestion=QuestionImage.objects.create(image=question_image)
question.image=imagequestion
except Exception:
pass
# Handle different question types # Handle different question types
if question_type == 'true_false': if question_type == 'true_false':
@@ -867,17 +239,6 @@ def edit_question(request, pk):
{'value': 'Falsch', 'is_correct': not correct_answer} {'value': 'Falsch', 'is_correct': not correct_answer}
] ]
} }
elif question_type == 'input':
correct_answer = request.POST.get('correct_answer')
json_data = {
'type': question_type,
'question': question_text,
'options': [
{'value': correct_answer,'is_correct':True}
]
}
elif question_type == 'multiple_choice': elif question_type == 'multiple_choice':
options = [] options = []
i = 1 i = 1
@@ -906,90 +267,19 @@ def edit_question(request, pk):
'options': options '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.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() question.save()
for img in QuestionImage.objects.all():
try:
img.delete()
img.image.delete(save=False)
except:
pass
#return modified(request, question.quiz_id.pk)
return redirect('library:detail_quiz', pk=question.quiz_id.pk) return redirect('library:detail_quiz', pk=question.quiz_id.pk)
else: else:
template_name = f'library/question/question_{question_type}.html' template_name = f'library/question/question_{question_type}.html'
context = { context = {
'credits':question.credits,
'question': {'data': question_data}, # Wrap in data structure expected by template 'question': {'data': question_data}, # Wrap in data structure expected by template
'quiz_id': question.quiz_id.pk, 'quiz_id': question.quiz_id.pk,
'quiz': question.quiz_id, 'quiz': question.quiz_id
'time_per_question': question.time_per_question,
'image': question_image,
} }
return render(request, template_name, context) return render(request, template_name, context)
# Frage löschen # Frage löschen
@login_required @login_required
def delete_question(request, pk): def delete_question(request, pk):
@@ -1001,11 +291,5 @@ def delete_question(request, pk):
if request.method == 'POST': if request.method == 'POST':
question.delete() question.delete()
for img in QuestionImage.objects.all():
try:
img.delete()
img.image.delete(save=False)
except:
pass
return redirect('library:detail_quiz', pk=question.quiz_id.pk) return redirect('library:detail_quiz', pk=question.quiz_id.pk)
return render(request, 'library/delete_confirmation.html', {'object': question}) return render(request, 'library/delete_confirmation.html', {'object': question})

1489
django/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,20 +0,0 @@
{
"name": "django",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"@tailwindcss/cli": "^4.1.5",
"sortablejs": "^1.15.6",
"tailwindcss": "^4.1.5"
},
"devDependencies": {
"license-checker": "^25.0.1"
}
}

View File

@@ -1,6 +1,3 @@
from django.contrib import admin from django.contrib import admin
from .models import QuizGame, QuizGameParticipant
# Register your models here. # Register your models here.
admin.site.register(QuizGame)
admin.site.register(QuizGameParticipant)

View File

@@ -1,528 +0,0 @@
import json
import asyncio
from channels.generic.websocket import AsyncWebsocketConsumer
from channels.db import database_sync_to_async
from django.utils import timezone
from datetime import timedelta
from django.urls import reverse
from .models import QuizGame, QuizGameParticipant
class GameConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.join_code = self.scope['url_route']['kwargs']['join_code']
self.game_group_name = f'game_{self.join_code}'
# Join game group
await self.channel_layer.group_add(
self.game_group_name,
self.channel_name
)
await self.accept()
async def disconnect(self, close_code):
# Leave game group
await self.channel_layer.group_discard(
self.game_group_name,
self.channel_name
)
async def receive(self, text_data):
text_data_json = json.loads(text_data)
message_type = text_data_json['type']
if message_type == 'submit_answer':
participant_id = text_data_json['participant_id']
answer = text_data_json['answer']
time_remaining = text_data_json.get('time_remaining', 0)
# Save answer and update participant score
await self.save_answer(participant_id, answer, time_remaining)
# Notify host about the new answer
await self.channel_layer.group_send(
self.game_group_name,
{
'type': 'participant_answer',
'answer': answer
}
)
elif message_type == 'get_answer_stats':
stats = await self.get_answer_stats()
await self.send(text_data=json.dumps({
'type': 'answer_stats',
'stats': stats
}))
elif message_type == 'update_participants':
participants = await self.get_participants()
await self.channel_layer.group_send(
self.game_group_name,
{
'type': 'participant_list_update',
'participants': participants
}
)
elif message_type == 'next_question':
host_id = text_data_json['host_id']
if await self.verify_host(host_id):
await self.advance_to_next_question()
elif message_type == 'finish_game':
host_id = text_data_json['host_id']
if await self.verify_host(host_id):
await self.finish_game()
elif message_type == 'advance_to_scores':
host_id = text_data_json['host_id']
if await self.verify_host(host_id):
await self.show_scores()
async def participant_answer(self, event):
await self.send(text_data=json.dumps({
'type': 'participant_answer',
'answer': event['answer']
}))
async def participant_list_update(self, event):
await self.send(text_data=json.dumps({
'type': 'participant_list_update',
'participants': event['participants']
}))
async def game_state_update(self, event):
await self.send(text_data=json.dumps({
'type': 'game_state_update',
'action': event['action'],
'redirect_url': event.get('redirect_url')
}))
@database_sync_to_async
def verify_host(self, host_id):
try:
quiz_game = QuizGame.objects.get(join_code=self.join_code)
return quiz_game.host_id == host_id
except QuizGame.DoesNotExist:
return False
@database_sync_to_async
def save_answer(self, participant_id, answer_index, time_remaining):
try:
participant = QuizGameParticipant.objects.get(
quiz_game__join_code=self.join_code,
participant_id=participant_id
)
quiz_game = participant.quiz_game
current_question = quiz_game.quiz_id.questions.all()[quiz_game.current_question_index]
# Check if answer is correct
is_correct = False
if answer_index >= 0: # -1 means timeout
is_correct = current_question.data['options'][answer_index]['is_correct']
# Calculate score based on correctness and time
score = 0
if is_correct:
base_score = 1000
time_factor = time_remaining / 30000 # 30 seconds max
score = int(base_score * (0.5 + 0.5 * time_factor))
participant.last_score=score
participant.score += score
participant.last_answer_correct = is_correct
participant.save()
return True
except (QuizGameParticipant.DoesNotExist, IndexError):
return False
@database_sync_to_async
def get_answer_stats(self):
try:
quiz_game = QuizGame.objects.get(join_code=self.join_code)
participants = QuizGameParticipant.objects.filter(quiz_game=quiz_game)
# Count answers for each option
stats = {}
for participant in participants:
if participant.last_answer is not None:
stats[participant.last_answer] = stats.get(participant.last_answer, 0) + 1
return stats
except QuizGame.DoesNotExist:
return {}
@database_sync_to_async
def get_participants(self):
try:
quiz_game = QuizGame.objects.get(join_code=self.join_code)
participants = QuizGameParticipant.objects.filter(quiz_game=quiz_game)
return [
{
'id': p.participant_id,
'display_name': p.display_name,
'score': p.score
}
for p in participants
]
except QuizGame.DoesNotExist:
return []
@database_sync_to_async
def _advance_to_next_question(self):
try:
quiz_game = QuizGame.objects.get(join_code=self.join_code)
quiz = quiz_game.quiz_id
# Move to next question
quiz_game.current_question_index += 1
# Check if we've reached the end
if quiz_game.current_question_index >= quiz.questions.count():
quiz_game.current_state = 'finished'
quiz_game.save()
return 'finished'
else:
# Update game state and start time
quiz_game.current_state = 'question'
quiz_game.question_start_time = timezone.now()
quiz_game.save()
return 'question'
except QuizGame.DoesNotExist:
return None
async def advance_to_next_question(self):
result = await self._advance_to_next_question()
if result == 'finished':
# Notify clients to redirect to finished page
await self.channel_layer.group_send(
self.game_group_name,
{
'type': 'game_state_update',
'action': 'finish_game',
'redirect_url': reverse('play:finished', kwargs={'join_code': self.join_code})
}
)
elif result == 'question':
# Notify clients to redirect to next question
await self.channel_layer.group_send(
self.game_group_name,
{
'type': 'game_state_update',
'action': 'next_question',
'redirect_url': reverse('play:question', kwargs={'join_code': self.join_code})
}
)
@database_sync_to_async
def _show_scores(self):
try:
quiz_game = QuizGame.objects.get(join_code=self.join_code)
quiz_game.current_state = 'scores'
quiz_game.save()
return True
except QuizGame.DoesNotExist:
return False
async def show_scores(self):
success = await self._show_scores()
if success:
# Notify clients to redirect to scores page
await self.channel_layer.group_send(
self.game_group_name,
{
'type': 'game_state_update',
'action': 'show_scores',
'redirect_url': reverse('play:scores', kwargs={'join_code': self.join_code})
}
)
@database_sync_to_async
def _finish_game(self):
try:
quiz_game = QuizGame.objects.get(join_code=self.join_code)
quiz_game.current_state = 'finished'
quiz_game.save()
return True
except QuizGame.DoesNotExist:
return False
async def finish_game(self):
success = await self._finish_game()
if success:
# Notify clients to redirect to finished page
await self.channel_layer.group_send(
self.game_group_name,
{
'type': 'game_state_update',
'action': 'finish_game',
'redirect_url': reverse('play:finished', kwargs={'join_code': self.join_code})
}
)
class LobbyConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.join_code = self.scope['url_route']['kwargs']['join_code']
self.room_group_name = f'game_{self.join_code}'
self.heartbeat_task = None
# Join room group
await self.channel_layer.group_add(
self.room_group_name,
self.channel_name
)
await self.accept()
# Start heartbeat check
self.heartbeat_task = asyncio.create_task(self.check_participants_heartbeat())
# Send current participants list
participants = await self.get_participants()
await self.send(text_data=json.dumps({
'type': 'participant_list',
'participants': participants
}))
async def disconnect(self, close_code):
# Cancel heartbeat task
if self.heartbeat_task:
self.heartbeat_task.cancel()
try:
await self.heartbeat_task
except asyncio.CancelledError:
pass
# Leave room group
await self.channel_layer.group_discard(
self.room_group_name,
self.channel_name
)
async def receive(self, text_data):
text_data_json = json.loads(text_data)
message_type = text_data_json['type']
if message_type == 'heartbeat':
# Update participant's last activity timestamp
participant_id = text_data_json.get('participant_id')
if participant_id:
await self._update_participant_heartbeat(participant_id)
elif message_type == 'update_participants':
participants = await self.get_participants()
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'participant_list_update',
'participants': participants
}
)
elif message_type == 'start_game':
# Verify sender is host
sender_id = text_data_json.get('host_id')
if await self._is_host(sender_id):
# Update game state and notify all participants
await self._start_game()
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'game_state_update',
'action': 'start_game',
'redirect_url': f'/play/game/{self.join_code}/0'
}
)
async def game_state_update(self, event):
# Send game state update to WebSocket
await self.send(text_data=json.dumps({
'type': 'game_state_update',
'action': event['action'],
'redirect_url': event.get('redirect_url')
}))
@database_sync_to_async
def _start_game(self):
game = QuizGame.objects.get(join_code=self.join_code)
game.current_state = 'question'
game.current_question_index = 0
game.question_start_time = timezone.now()
game.save()
async def receive(self, text_data):
text_data_json = json.loads(text_data)
message_type = text_data_json['type']
if message_type == 'heartbeat':
# Update participant's last activity timestamp
participant_id = text_data_json.get('participant_id')
if participant_id:
await self._update_participant_heartbeat(participant_id)
elif message_type == 'update_participants':
participants = await self.get_participants()
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'participant_list_update',
'participants': participants
}
)
elif message_type == 'start_game':
# Verify sender is host
sender_id = text_data_json.get('host_id')
if await self._is_host(sender_id):
# Update game state and notify all participants
await self._start_game()
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'game_state_update',
'action': 'start_game',
'redirect_url': f'/play/game/{self.join_code}/0'
}
)
elif message_type == 'submit_answer':
participant_id = text_data_json.get('participant_id')
answer = text_data_json.get('answer')
time_remaining = text_data_json.get('time_remaining')
if participant_id:
score = await self._process_answer(participant_id, answer, time_remaining)
await self.send(text_data=json.dumps({
'type': 'answer_processed',
'score': score
}))
elif message_type == 'next_question':
sender_id = text_data_json.get('host_id')
if await self._is_host(sender_id):
next_state = await self._advance_game_state()
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'game_state_update',
'action': next_state['action'],
'redirect_url': next_state['redirect_url']
}
)
async def participant_list_update(self, event):
participants = event['participants']
await self.send(text_data=json.dumps({
'type': 'participant_list',
'participants': participants
}))
@database_sync_to_async
def get_participants(self):
game = QuizGame.objects.get(join_code=self.join_code)
participants = QuizGameParticipant.objects.filter(quiz_game=game)
return [{'id': str(p.participant_id), 'name': p.display_name} for p in participants]
@database_sync_to_async
def _update_participant_heartbeat(self, participant_id):
try:
participant = QuizGameParticipant.objects.get(participant_id=participant_id)
participant.save() # This will update last_heartbeat due to auto_now=True
except QuizGameParticipant.DoesNotExist:
pass
@database_sync_to_async
def _is_host(self, host_id):
try:
return QuizGame.objects.filter(join_code=self.join_code, host_id=host_id).exists()
except QuizGame.DoesNotExist:
return False
@database_sync_to_async
def _start_game(self):
game = QuizGame.objects.get(join_code=self.join_code)
game.current_state = 'question'
game.current_question_index = 0
game.question_start_time = timezone.now()
game.save()
@database_sync_to_async
def _process_answer(self, participant_id, answer, time_remaining):
game = QuizGame.objects.get(join_code=self.join_code)
participant = QuizGameParticipant.objects.get(participant_id=participant_id)
question = game.quiz_id.questions.all()[game.current_question_index]
# Load question data
question_data = json.loads(question.data)
correct_answer = question_data.get('correct_answer')
# Calculate score based on correctness and time
score = 0
if answer == correct_answer:
# Base score for correct answer + bonus for speed
score = 1000 + int(time_remaining * 10) # 10 points per remaining second
participant.last_score= score
participant.score += score
participant.save()
return score
@database_sync_to_async
def _advance_game_state(self):
game = QuizGame.objects.get(join_code=self.join_code)
total_questions = game.quiz_id.questions.count()
if game.current_state == 'question':
game.current_state = 'scores'
game.save()
return {
'action': 'show_scores',
'redirect_url': f'/play/game/{self.join_code}/scores'
}
elif game.current_state == 'scores':
if game.current_question_index + 1 < total_questions:
game.current_state = 'question'
game.current_question_index += 1
game.question_start_time = timezone.now()
game.save()
return {
'action': 'next_question',
'redirect_url': f'/play/game/{self.join_code}/{game.current_question_index}'
}
else:
game.current_state = 'finished'
game.save()
return {
'action': 'game_finished',
'redirect_url': f'/play/game/{self.join_code}/finished'
}
@database_sync_to_async
def _remove_inactive_participants(self):
# Remove participants who haven't sent a heartbeat in the last 30 seconds
timeout = timezone.now() - timedelta(seconds=30)
quiz_game = QuizGame.objects.get(join_code=self.join_code)
QuizGameParticipant.objects.filter(
quiz_game=quiz_game,
last_heartbeat__lt=timeout
).delete()
async def check_participants_heartbeat(self):
while True:
try:
await asyncio.sleep(10) # Check every 10 seconds
await self._remove_inactive_participants()
# Send updated participant list
participants = await self.get_participants()
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'participant_list_update',
'participants': participants
}
)
except asyncio.CancelledError:
break
except Exception as e:
print(f'Error in heartbeat check: {e}')
await asyncio.sleep(10) # Wait before retrying

Some files were not shown because too many files have changed in this diff Show More