Compare commits
4 Commits
141-richti
...
30-chat-ap
| Author | SHA1 | Date | |
|---|---|---|---|
| 9b13749a29 | |||
|
|
b6831616cc | ||
| 889134784a | |||
| ab6e27f80e |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,10 +1,6 @@
|
||||
__pycache__
|
||||
.venv
|
||||
.env
|
||||
db.sqlite3
|
||||
tailwindcss.exe
|
||||
t-style.css
|
||||
dump.rdb
|
||||
media
|
||||
node_modules
|
||||
staticfiles
|
||||
73
README.md
73
README.md
@@ -7,7 +7,6 @@
|
||||
3. [Superuser erstellen](#superuser-erstellen)
|
||||
4. [Tailwind-Nutzung](#tailwind-nutzung)
|
||||
5. [Issue - Merge Request - Merge](#issue-mergerequest-merge)
|
||||
6. [Konfiguration](#konfiguration)
|
||||
|
||||
## Initialisierung des Projekts
|
||||
|
||||
@@ -41,70 +40,24 @@ Um das Projekt erfolgreich zu starten, folge diesen Schritten:
|
||||
```
|
||||
7. Die Datenbank migrieren:
|
||||
```sh
|
||||
python django/manage.py migrate
|
||||
python3 core/manage.py migrate
|
||||
```
|
||||
|
||||
## Starten des Servers
|
||||
|
||||
### 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:
|
||||
```sh
|
||||
source .venv/bin/activate # Linux
|
||||
.venv\Scripts\activate # Windows
|
||||
```
|
||||
|
||||
2. Starte den Daphne-Server:
|
||||
```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:
|
||||
- Standardport (8000):
|
||||
```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:
|
||||
- Bestimmten Port (z. B. 9000):
|
||||
```sh
|
||||
python3 core/manage.py collectstatic
|
||||
python3 core/manage.py runserver 9000
|
||||
```
|
||||
|
||||
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!
|
||||
|
||||
## Superuser erstellen
|
||||
@@ -147,10 +100,10 @@ npx @tailwindcss/cli -i <INPUT-DATEI> -o <OUTPUT-DATEI> --watch --minify
|
||||
|
||||
```sh
|
||||
# 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 '\'):
|
||||
npx @tailwindcss/cli -i ./static/css/t-input.css -o ./static/css/t-style.css --watch --minify
|
||||
# unter Windows mit NPM:
|
||||
npx @tailwindcss/cli -i .\core\static\homepage\t-input.css -o .\core\static\homepage\t-style.css --watch --minify
|
||||
```
|
||||
|
||||
## Issue - Merge Request - Merge
|
||||
@@ -159,14 +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.
|
||||
|
||||
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.
|
||||
|
||||
## 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 |
|
||||
@@ -6,15 +6,11 @@ from django import forms
|
||||
class RegisterForm(UserCreationForm):
|
||||
class Meta:
|
||||
model=User
|
||||
fields = ['username','password1','password2', "email"]
|
||||
fields = ['username','password1','password2']
|
||||
|
||||
username = forms.CharField(
|
||||
widget=forms.TextInput(attrs={'placeholder': 'BENUTZERNAME'})
|
||||
)
|
||||
email = forms.EmailField(
|
||||
widget=forms.TextInput(attrs={'placeholder': 'E-MAIL'})
|
||||
)
|
||||
|
||||
password1 = forms.CharField(
|
||||
widget=forms.PasswordInput(attrs={'placeholder': 'PASSWORT'})
|
||||
)
|
||||
@@ -23,3 +19,4 @@ class RegisterForm(UserCreationForm):
|
||||
)
|
||||
|
||||
|
||||
|
||||
16
core/accounts/urls.py
Normal file
16
core/accounts/urls.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from django.urls import path # type: ignore
|
||||
from . import views
|
||||
from django.contrib.auth.views import LogoutView
|
||||
from django.contrib.auth.views import LoginView
|
||||
|
||||
app_name = 'accounts' # 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('', views.home, name='home'),
|
||||
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('register/', views.register, name='register'),
|
||||
]
|
||||
19
core/accounts/views.py
Normal file
19
core/accounts/views.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from django.shortcuts import render, redirect
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from .forms import RegisterForm
|
||||
|
||||
# Create your views here.
|
||||
@login_required
|
||||
def home(request):
|
||||
return render(request, 'accounts/home.html')
|
||||
|
||||
def register(response):
|
||||
if response.method == "POST":
|
||||
form = RegisterForm(response.POST)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
return redirect("home")
|
||||
else:
|
||||
form = RegisterForm()
|
||||
|
||||
return render(response, "accounts/register.html", {"form":form})
|
||||
40
core/chat/consumers.py
Normal file
40
core/chat/consumers.py
Normal file
@@ -0,0 +1,40 @@
|
||||
import json
|
||||
|
||||
from asgiref.sync import async_to_sync
|
||||
from channels.generic.websocket import WebsocketConsumer
|
||||
|
||||
|
||||
class ChatConsumer(WebsocketConsumer):
|
||||
def connect(self):
|
||||
self.room_name = self.scope["url_route"]["kwargs"]["room_name"]
|
||||
self.room_group_name = f"chat_{self.room_name}"
|
||||
|
||||
# Join room group
|
||||
async_to_sync(self.channel_layer.group_add)(
|
||||
self.room_group_name, self.channel_name
|
||||
)
|
||||
|
||||
self.accept()
|
||||
|
||||
def disconnect(self, close_code):
|
||||
# Leave room group
|
||||
async_to_sync(self.channel_layer.group_discard)(
|
||||
self.room_group_name, self.channel_name
|
||||
)
|
||||
|
||||
# Receive message from WebSocket
|
||||
def receive(self, text_data):
|
||||
text_data_json = json.loads(text_data)
|
||||
message = text_data_json["message"]
|
||||
|
||||
# Send message to room group
|
||||
async_to_sync(self.channel_layer.group_send)(
|
||||
self.room_group_name, {"type": "chat.message", "message": message}
|
||||
)
|
||||
|
||||
# Receive message from room group
|
||||
def chat_message(self, event):
|
||||
message = event["message"]
|
||||
|
||||
# Send message to WebSocket
|
||||
self.send(text_data=json.dumps({"message": message}))
|
||||
8
core/chat/routing.py
Normal file
8
core/chat/routing.py
Normal file
@@ -0,0 +1,8 @@
|
||||
# chat/routing.py
|
||||
from django.urls import re_path
|
||||
|
||||
from . import consumers
|
||||
|
||||
websocket_urlpatterns = [
|
||||
re_path(r"ws/chat/(?P<room_name>\w+)/$", consumers.ChatConsumer.as_asgi()),
|
||||
]
|
||||
9
core/chat/urls.py
Normal file
9
core/chat/urls.py
Normal file
@@ -0,0 +1,9 @@
|
||||
# chat/urls.py
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path("", views.index, name="index"),
|
||||
path("<str:room_name>/", views.room, name="room"),
|
||||
]
|
||||
8
core/chat/views.py
Normal file
8
core/chat/views.py
Normal file
@@ -0,0 +1,8 @@
|
||||
# chat/views.py
|
||||
from django.shortcuts import render
|
||||
|
||||
def index(request):
|
||||
return render(request, "chat/index.html")
|
||||
|
||||
def room(request, room_name):
|
||||
return render(request, "chat/room.html", {"room_name": room_name})
|
||||
3
core/components/admin.py
Normal file
3
core/components/admin.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
6
core/components/apps.py
Normal file
6
core/components/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ComponentsConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'components'
|
||||
3
core/components/models.py
Normal file
3
core/components/models.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
14
core/components/urls.py
Normal file
14
core/components/urls.py
Normal 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')
|
||||
|
||||
]
|
||||
9
core/components/views.py
Normal file
9
core/components/views.py
Normal 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}) #
|
||||
34
core/core/asgi.py
Normal file
34
core/core/asgi.py
Normal file
@@ -0,0 +1,34 @@
|
||||
"""
|
||||
ASGI config for core project.
|
||||
|
||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from channels.auth import AuthMiddlewareStack
|
||||
from channels.routing import ProtocolTypeRouter, URLRouter
|
||||
from channels.security.websocket import AllowedHostsOriginValidator
|
||||
from django.core.asgi import get_asgi_application
|
||||
from django.core.asgi import get_asgi_application
|
||||
from channels.routing import ProtocolTypeRouter
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
|
||||
|
||||
#application = get_asgi_application()
|
||||
|
||||
django_asgi_app = get_asgi_application()
|
||||
|
||||
from chat.routing import websocket_urlpatterns
|
||||
|
||||
application = ProtocolTypeRouter(
|
||||
{
|
||||
"http": django_asgi_app,
|
||||
"websocket": AllowedHostsOriginValidator(
|
||||
AuthMiddlewareStack(URLRouter(websocket_urlpatterns))
|
||||
),
|
||||
}
|
||||
)
|
||||
@@ -10,58 +10,44 @@ For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/5.1/ref/settings/
|
||||
"""
|
||||
|
||||
import json, os
|
||||
from pathlib import Path
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
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
|
||||
# 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!
|
||||
#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')
|
||||
SECRET_KEY = 'django-insecure-#+s307$rxkts=8@+_^i)8)n1b4*y4za1xz!mvv(h@!+n9!mp9)'
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
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', '*', '172.22.10.111']
|
||||
|
||||
# Konfigurationsdatei für Projekt
|
||||
with open(BASE_DIR / 'config' / 'qivip_config.json') as config_file:
|
||||
QIVIP_CONFIG = json.load(config_file)
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'daphne',
|
||||
'chat',
|
||||
'library.apps.LibraryConfig',
|
||||
'homepage.apps.HomepageConfig',
|
||||
'components.apps.ComponentsConfig',
|
||||
'accounts.apps.AccountsConfig',
|
||||
'play',
|
||||
'play.apps.PlayConfig',
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'channels',
|
||||
'django_cleanup',
|
||||
'django.contrib.humanize',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'whitenoise.middleware.WhiteNoiseMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
@@ -89,17 +75,6 @@ TEMPLATES = [
|
||||
]
|
||||
|
||||
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
|
||||
@@ -112,15 +87,6 @@ DATABASES = {
|
||||
}
|
||||
}
|
||||
|
||||
STORAGES = {
|
||||
"staticfiles": {
|
||||
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
|
||||
},
|
||||
"default": {
|
||||
"BACKEND": "django.core.files.storage.FileSystemStorage",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
|
||||
@@ -157,32 +123,25 @@ USE_TZ = True
|
||||
# https://docs.djangoproject.com/en/5.1/howto/static-files/
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
STATIC_ROOT = BASE_DIR / "staticfiles"
|
||||
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
|
||||
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
||||
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
|
||||
# Daphne
|
||||
ASGI_APPLICATION = "core.asgi.application"
|
||||
|
||||
# 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'
|
||||
# Channels
|
||||
ASGI_APPLICATION = "core.asgi.application"
|
||||
CHANNEL_LAYERS = {
|
||||
"default": {
|
||||
"BACKEND": "channels_redis.core.RedisChannelLayer",
|
||||
"CONFIG": {
|
||||
"hosts": [("127.0.0.1", 8000)],
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -14,8 +14,6 @@ Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
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.urls import path, include
|
||||
|
||||
@@ -25,7 +23,5 @@ urlpatterns = [
|
||||
path('', include('homepage.urls')),
|
||||
path('play/', include('play.urls')),
|
||||
path('library/', include('library.urls')),
|
||||
path('chat/', include('chat.urls')),
|
||||
]
|
||||
|
||||
if settings.DEBUG:
|
||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
3
core/homepage/admin.py
Normal file
3
core/homepage/admin.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
3
core/homepage/models.py
Normal file
3
core/homepage/models.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
9
core/homepage/views.py
Normal file
9
core/homepage/views.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from django.shortcuts import render, redirect
|
||||
|
||||
def home(request):
|
||||
return render(request, 'homepage/home.html')
|
||||
|
||||
def impress(request):
|
||||
return render(request, 'homepage/impress.html')
|
||||
def privacy(request):
|
||||
return render(request, 'homepage/privacy.html')
|
||||
0
core/library/__init__.py
Normal file
0
core/library/__init__.py
Normal file
31
core/library/admin.py
Normal file
31
core/library/admin.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from django.contrib import admin
|
||||
from .models import QivipQuiz, QivipQuestion, QuizCategory, Tag
|
||||
|
||||
# Für das QivipQuiz Modell
|
||||
class QivipQuizAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'user_id', 'status', 'category', 'creation_date', 'update_date') # Welche Felder sollen in der Übersicht angezeigt werden
|
||||
search_fields = ('name', 'user_id__username', 'category__name') # Suchfelder
|
||||
list_filter = ('status', 'category') # Filteroptionen
|
||||
|
||||
# Für das QivipQuestion Modell
|
||||
class QivipQuestionAdmin(admin.ModelAdmin):
|
||||
list_display = ('quiz_id', 'data', 'creation_date', 'update_date')
|
||||
search_fields = ('data',)
|
||||
list_filter = ('quiz_id',)
|
||||
|
||||
# Für das QuizCategory Modell
|
||||
class QuizCategoryAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'slug', 'description') # Zeige Name, Slug und Beschreibung an
|
||||
search_fields = ('name',)
|
||||
prepopulated_fields = {'slug': ('name',)} # Erstelle automatisch den Slug basierend auf dem Namen
|
||||
|
||||
# Für das Tag Modell
|
||||
class TagAdmin(admin.ModelAdmin):
|
||||
list_display = ('name',) # Zeige nur den Namen des Tags
|
||||
search_fields = ('name',)
|
||||
|
||||
# Registrierung der Modelle im Admin
|
||||
admin.site.register(QivipQuiz, QivipQuizAdmin)
|
||||
admin.site.register(QivipQuestion, QivipQuestionAdmin)
|
||||
admin.site.register(QuizCategory, QuizCategoryAdmin)
|
||||
admin.site.register(Tag, TagAdmin)
|
||||
12
core/library/forms.py
Normal file
12
core/library/forms.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from django import forms
|
||||
from .models import QivipQuiz, QivipQuestion
|
||||
|
||||
class QuizForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = QivipQuiz
|
||||
fields = ['name', 'description', 'status', 'category', 'tags',"difficulty"]
|
||||
|
||||
class QuestionForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = QivipQuestion
|
||||
fields = ['quiz_id', 'data']
|
||||
0
core/library/migrations/__init__.py
Normal file
0
core/library/migrations/__init__.py
Normal file
65
core/library/models.py
Normal file
65
core/library/models.py
Normal file
@@ -0,0 +1,65 @@
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
from django.utils.text import slugify
|
||||
import uuid
|
||||
|
||||
class QivipQuiz(models.Model):
|
||||
STATUS_VALUES = {
|
||||
"public": "Öffentlich",
|
||||
"hidden": "Versteckt",
|
||||
"private": "Privat",
|
||||
}
|
||||
DIFFICULTY_VALUES = {
|
||||
"1": "1",
|
||||
"2": "2",
|
||||
"3": "3",
|
||||
"4": "4",
|
||||
"5": "5",
|
||||
"not defined":"nicht gesetzt",
|
||||
|
||||
}
|
||||
|
||||
uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
|
||||
user_id = models.ForeignKey(User, on_delete=models.CASCADE, related_name='quiz')
|
||||
creation_date = models.DateTimeField(auto_now_add=True)
|
||||
update_date = models.DateTimeField(auto_now=True)
|
||||
status = models.CharField(max_length=10, choices=list(STATUS_VALUES.items()), default="public")
|
||||
category = models.ForeignKey('QuizCategory', related_name='quiz', on_delete=models.CASCADE)
|
||||
tags = models.ManyToManyField('Tag', blank=True)
|
||||
name = models.CharField(max_length=255)
|
||||
description = models.TextField(blank=True, null=True, default="In dem Quiz geht es um ...")
|
||||
difficulty= models.CharField(max_length=11, choices=list(DIFFICULTY_VALUES.items()), default="not defined", help_text="1: niedrigste Schwierigkeit und 5: höchste Schwierigkeit")
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
# Create your models here.
|
||||
class QivipQuestion(models.Model):
|
||||
uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
|
||||
quiz_id = models.ForeignKey(QivipQuiz, on_delete=models.CASCADE, related_name='question')
|
||||
creation_date = models.DateTimeField(auto_now_add=True)
|
||||
update_date = models.DateTimeField(auto_now=True)
|
||||
data = models.TextField()
|
||||
|
||||
def __str__(self):
|
||||
return self.data[:50]
|
||||
|
||||
class Tag(models.Model):
|
||||
name = models.CharField(max_length=100, unique=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class QuizCategory(models.Model):
|
||||
name = models.CharField(max_length=100, unique=True)
|
||||
description = models.TextField(blank=True, null=True)
|
||||
slug = models.SlugField(unique=True, blank=True)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.slug:
|
||||
self.slug = slugify(self.name)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@@ -1,15 +1,10 @@
|
||||
from django.urls import path
|
||||
|
||||
from django.conf import settings
|
||||
from . import views
|
||||
from django.conf.urls.static import static
|
||||
|
||||
app_name = 'library'
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.overview_quiz, name='overview_quiz'),
|
||||
path('my/', views.overview_quiz_my, name='overview_quiz_my'),
|
||||
path('favorites/', views.overview_quiz_favorites, name='overview_quiz_favorites'),
|
||||
|
||||
path('new/', views.new_quiz, name='new_quiz'),
|
||||
path('edit/<int:pk>/', views.edit_quiz, name='edit_quiz'),
|
||||
path('delete/<int:pk>/', views.delete_quiz, name='delete_quiz'),
|
||||
@@ -17,8 +12,4 @@ urlpatterns = [
|
||||
path('question/new/', views.new_question, name='new_question'),
|
||||
path('question/edit/<int:pk>/', views.edit_question, name='edit_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("quiz-names-json/", views.quiz_names_json, name="quiz_names_json"),
|
||||
|
||||
]
|
||||
295
core/library/views.py
Normal file
295
core/library/views.py
Normal file
@@ -0,0 +1,295 @@
|
||||
from django.shortcuts import render, redirect, get_object_or_404
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.models import User
|
||||
from django.urls import reverse
|
||||
from django.contrib import messages
|
||||
from .models import QivipQuiz, QivipQuestion
|
||||
from .forms import QuizForm, QuestionForm
|
||||
import json
|
||||
|
||||
# Übersicht aller Quizze
|
||||
@login_required
|
||||
def overview_quiz(request):
|
||||
quizzes = QivipQuiz.objects.filter(user_id=request.user)
|
||||
return render(request, 'library/overview_quiz.html', {'quizzes': quizzes})
|
||||
|
||||
# Neues Quiz erstellen
|
||||
@login_required
|
||||
def new_quiz(request):
|
||||
if request.method == 'POST':
|
||||
form = QuizForm(request.POST)
|
||||
if form.is_valid():
|
||||
quiz = form.save(commit=False)
|
||||
quiz.user_id = request.user
|
||||
quiz.save()
|
||||
form.save_m2m() # Speichert die Many-to-Many Beziehungen (Tags)
|
||||
return redirect('library:edit_quiz', pk=quiz.pk)
|
||||
else:
|
||||
form = QuizForm()
|
||||
return render(request, 'library/form.html', {'form': form})
|
||||
|
||||
# Quiz bearbeiten
|
||||
@login_required
|
||||
def edit_quiz(request, pk):
|
||||
quiz = get_object_or_404(QivipQuiz, pk=pk, user_id=request.user)
|
||||
if request.method == 'POST':
|
||||
form = QuizForm(request.POST, instance=quiz)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
return redirect('library:overview_quiz')
|
||||
else:
|
||||
form = QuizForm(instance=quiz)
|
||||
return render(request, 'library/form.html', {'form': form})
|
||||
|
||||
# Quiz löschen
|
||||
@login_required
|
||||
def delete_quiz(request, pk):
|
||||
quiz = get_object_or_404(QivipQuiz, pk=pk, user_id=request.user)
|
||||
if request.method == 'POST':
|
||||
quiz.delete()
|
||||
return redirect('library:overview_quiz')
|
||||
return render(request, 'library/delete_confirmation.html', {'object': quiz})
|
||||
|
||||
# Quiz anzeigen
|
||||
@login_required
|
||||
def detail_quiz(request, pk):
|
||||
quiz = get_object_or_404(QivipQuiz, pk=pk, user_id=request.user)
|
||||
questions = QivipQuestion.objects.filter(quiz_id=quiz)
|
||||
|
||||
# Parse JSON data for each question
|
||||
for question in questions:
|
||||
if question.data:
|
||||
question.data = json.loads(question.data)
|
||||
|
||||
context = {
|
||||
'quiz': quiz,
|
||||
'questions': questions
|
||||
}
|
||||
return render(request, 'library/detail_quiz.html', context)
|
||||
|
||||
# Übersicht aller Fragen
|
||||
@login_required
|
||||
def question_list(request):
|
||||
questions = QivipQuestion.objects.filter(quiz_id__user_id=request.user)
|
||||
|
||||
# Parse JSON data for each question
|
||||
for question in questions:
|
||||
if question.data:
|
||||
question.data = json.loads(question.data)
|
||||
|
||||
return render(request, 'library/question_list.html', {'questions': questions})
|
||||
|
||||
# Neue Frage erstellen
|
||||
@login_required
|
||||
def new_question(request):
|
||||
question_type = request.GET.get('type')
|
||||
quiz_id = request.GET.get('quiz_id')
|
||||
|
||||
if not quiz_id:
|
||||
messages.error(request, 'Quiz ID muss angegeben werden.')
|
||||
return redirect('library:overview_quiz')
|
||||
|
||||
try:
|
||||
quiz = QivipQuiz.objects.get(pk=quiz_id, user_id=request.user)
|
||||
except QivipQuiz.DoesNotExist:
|
||||
messages.error(request, 'Quiz nicht gefunden oder keine Berechtigung.')
|
||||
return redirect('library:overview_quiz')
|
||||
|
||||
if question_type not in ['multiple_choice', 'true_false']:
|
||||
base_url = reverse('library:new_question')
|
||||
return redirect(f'{base_url}?type=multiple_choice&quiz_id={quiz_id}')
|
||||
|
||||
if request.method == 'POST':
|
||||
question_text = request.POST.get('question', '')
|
||||
|
||||
# Handle different question types
|
||||
if question_type == 'true_false':
|
||||
correct_answer = request.POST.get('correct_answer') == 'true'
|
||||
json_data = {
|
||||
'type': question_type,
|
||||
'question': question_text,
|
||||
'options': [
|
||||
{'value': 'Wahr', 'is_correct': correct_answer},
|
||||
{'value': 'Falsch', 'is_correct': not correct_answer}
|
||||
]
|
||||
}
|
||||
elif question_type == 'multiple_choice':
|
||||
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}'),
|
||||
'is_correct': is_correct
|
||||
})
|
||||
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
|
||||
}
|
||||
|
||||
question = QivipQuestion()
|
||||
question.data = json.dumps(json_data)
|
||||
question.quiz_id = quiz
|
||||
question.save()
|
||||
return redirect('library:detail_quiz', pk=quiz.pk)
|
||||
else:
|
||||
# Initialize empty question data for new questions
|
||||
if question_type == 'true_false':
|
||||
question_data = {
|
||||
'type': question_type,
|
||||
'question': '',
|
||||
'options': [
|
||||
{'value': 'Wahr', 'is_correct': True},
|
||||
{'value': 'Falsch', 'is_correct': False}
|
||||
]
|
||||
}
|
||||
elif question_type == 'multiple_choice':
|
||||
question_data = {
|
||||
'type': question_type,
|
||||
'question': '',
|
||||
'options': [
|
||||
{'value': '', 'is_correct': False},
|
||||
{'value': '', 'is_correct': False}
|
||||
]
|
||||
}
|
||||
|
||||
template_name = f'library/question/question_{question_type}.html'
|
||||
context = {
|
||||
'question': question_data,
|
||||
'quiz_id': quiz_id,
|
||||
'quiz': quiz
|
||||
}
|
||||
|
||||
return render(request, template_name, context)
|
||||
|
||||
# Frage bearbeiten
|
||||
@login_required
|
||||
def edit_question(request, pk):
|
||||
question = get_object_or_404(QivipQuestion, pk=pk, quiz_id__user_id=request.user)
|
||||
if question.quiz_id.user_id != request.user:
|
||||
return redirect('library:question_list')
|
||||
|
||||
# Parse the existing JSON data
|
||||
try:
|
||||
question_data = json.loads(question.data) if question.data else {}
|
||||
question_type = question_data.get('type', 'multiple_choice')
|
||||
|
||||
# For true/false questions, get the correct answer from the options
|
||||
if question_type == 'true_false':
|
||||
# Default to true if no options exist
|
||||
correct_answer = True
|
||||
if question_data.get('options'):
|
||||
correct_answer = question_data['options'][0].get('is_correct', True)
|
||||
question_data = {
|
||||
'type': question_type,
|
||||
'question': question_data.get('question', ''),
|
||||
'correct_answer': correct_answer, # Add this for template compatibility
|
||||
'options': [
|
||||
{'value': 'Wahr', 'is_correct': correct_answer},
|
||||
{'value': 'Falsch', 'is_correct': not correct_answer}
|
||||
]
|
||||
}
|
||||
else:
|
||||
# For multiple choice questions
|
||||
question_data.setdefault('type', question_type)
|
||||
question_data.setdefault('question', '')
|
||||
question_data.setdefault('options', [])
|
||||
|
||||
except (json.JSONDecodeError, AttributeError):
|
||||
# Handle invalid JSON or None data
|
||||
question_type = question_data.get('type', 'multiple_choice')
|
||||
if question_type == 'true_false':
|
||||
question_data = {
|
||||
'type': question_type,
|
||||
'question': '',
|
||||
'correct_answer': True, # Default to true for new questions
|
||||
'options': [
|
||||
{'value': 'Wahr', 'is_correct': True},
|
||||
{'value': 'Falsch', 'is_correct': False}
|
||||
]
|
||||
}
|
||||
else:
|
||||
question_data = {
|
||||
'type': question_type,
|
||||
'question': '',
|
||||
'options': []
|
||||
}
|
||||
|
||||
if request.method == 'POST':
|
||||
question_text = request.POST.get('question', '')
|
||||
|
||||
# Handle different question types
|
||||
if question_type == 'true_false':
|
||||
correct_answer = request.POST.get('correct_answer') == 'true'
|
||||
json_data = {
|
||||
'type': question_type,
|
||||
'question': question_text,
|
||||
'options': [
|
||||
{'value': 'Wahr', 'is_correct': correct_answer},
|
||||
{'value': 'Falsch', 'is_correct': not correct_answer}
|
||||
]
|
||||
}
|
||||
elif question_type == 'multiple_choice':
|
||||
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}'),
|
||||
'is_correct': request.POST.get(f'correct_{i}') == '1'
|
||||
})
|
||||
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
|
||||
}
|
||||
|
||||
question.data = json.dumps(json_data)
|
||||
question.save()
|
||||
return redirect('library:detail_quiz', pk=question.quiz_id.pk)
|
||||
else:
|
||||
template_name = f'library/question/question_{question_type}.html'
|
||||
context = {
|
||||
'question': {'data': question_data}, # Wrap in data structure expected by template
|
||||
'quiz_id': question.quiz_id.pk,
|
||||
'quiz': question.quiz_id
|
||||
}
|
||||
|
||||
return render(request, template_name, context)
|
||||
|
||||
# Frage löschen
|
||||
@login_required
|
||||
def delete_question(request, pk):
|
||||
question = get_object_or_404(QivipQuestion, pk=pk, quiz_id__user_id=request.user)
|
||||
|
||||
# Parse JSON data for question
|
||||
if question.data:
|
||||
question.data = json.loads(question.data)
|
||||
|
||||
if request.method == 'POST':
|
||||
question.delete()
|
||||
return redirect('library:detail_quiz', pk=question.quiz_id.pk)
|
||||
return render(request, 'library/delete_confirmation.html', {'object': question})
|
||||
0
core/play/__init__.py
Normal file
0
core/play/__init__.py
Normal file
3
core/play/admin.py
Normal file
3
core/play/admin.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
0
core/play/migrations/__init__.py
Normal file
0
core/play/migrations/__init__.py
Normal file
3
core/play/models.py
Normal file
3
core/play/models.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
3
core/play/tests.py
Normal file
3
core/play/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
9
core/play/urls.py
Normal file
9
core/play/urls.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
app_name = 'play'
|
||||
|
||||
urlpatterns = [
|
||||
path('lobby', views.lobby, name='lobby'),
|
||||
path('join', views.join_game, name='join_game'),
|
||||
]
|
||||
8
core/play/views.py
Normal file
8
core/play/views.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
def lobby(request):
|
||||
return render(request, 'play/lobby.html')
|
||||
|
||||
def join_game(request):
|
||||
return render(request, 'play/join_game.html')
|
||||
53
core/static/css/t-input.css
Normal file
53
core/static/css/t-input.css
Normal file
@@ -0,0 +1,53 @@
|
||||
@import 'tailwindcss';
|
||||
|
||||
qp-container {
|
||||
@apply max-w-xl w-full mx-auto;
|
||||
}
|
||||
|
||||
nav div ul.qp-nav-list li {
|
||||
@apply text-white hover:scale-110 hover:border-b-2 hover:border-white transition duration-200;
|
||||
}
|
||||
|
||||
a.qp-a-button {
|
||||
@apply p-6 rounded-md font-extrabold hover:scale-105 transition duration-300 shadow-md;
|
||||
}
|
||||
a.qp-a-button-small {
|
||||
@apply p-1 rounded-md hover:scale-105 transition duration-300 shadow-md;
|
||||
}
|
||||
.register input{@apply p-3 rounded-full bg-gray-200 focus:scale-105 transition duration-200 font-black w-full;}
|
||||
|
||||
.input-group {
|
||||
@apply flex flex-col gap-4 max-w-md mx-auto p-6 bg-white rounded-xl shadow-lg;
|
||||
}
|
||||
|
||||
.input-group form {
|
||||
@apply flex flex-col gap-4;
|
||||
}
|
||||
|
||||
.input-group form p {
|
||||
@apply flex flex-col gap-2;
|
||||
}
|
||||
|
||||
.input-group form p label {
|
||||
@apply text-gray-900 font-semibold text-sm;
|
||||
}
|
||||
|
||||
.input-group form p input {
|
||||
@apply p-3 rounded-full bg-gray-200 focus:scale-105 transition duration-200 font-black
|
||||
focus:outline-none focus:ring-2 focus:ring-blue-400 focus:bg-blue-100;
|
||||
}
|
||||
|
||||
.input-group form p select {
|
||||
@apply p-3 rounded-md bg-gray-200 focus:scale-105 transition duration-200 font-black
|
||||
focus:outline-none focus:ring-2 focus:ring-blue-400 focus:bg-blue-100;
|
||||
}
|
||||
|
||||
.input-group form p textarea {
|
||||
@apply p-3 rounded-xl bg-gray-200 focus:scale-105 transition duration-200 font-black
|
||||
focus:outline-none focus:ring-2 focus:ring-blue-400 focus:bg-blue-100 min-h-[100px];
|
||||
}
|
||||
|
||||
.input-group form p button {
|
||||
@apply p-3 rounded-full bg-indigo-500 hover:bg-indigo-600 text-white focus:scale-105
|
||||
transition duration-200 font-black shadow-md hover:shadow-lg;
|
||||
}
|
||||
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.7 KiB |
25
core/templates/accounts/home.html
Normal file
25
core/templates/accounts/home.html
Normal file
@@ -0,0 +1,25 @@
|
||||
{% extends 'base.html'%}
|
||||
{% block title %}Home{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
|
||||
<div class="flex justify-center items-center mt-12 ">
|
||||
<div class="input-group p-4 m-2 border-blue-600 border-2 rounded-xl shadow-md">
|
||||
<div class="grid place-content-center h-120">
|
||||
|
||||
<h1 class="text-center m-4 text-3xl lg:text-6xl">Willkommen, <b>{{ request.user.username }}</b>!</h1>
|
||||
|
||||
<form action="{% url 'accounts:logout' %}" method="post">
|
||||
{% csrf_token %}
|
||||
<button class="bg-blue-600 text-white p-3 rounded-full font-black" type="submit">Abmelden</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<span class="text-[clamp(0.6rem,5vw,1rem)] font-black overflow-x-auto mt-1 ml-0.75 text-blue-600 p-0.75 bg-white">qivip
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
@@ -4,7 +4,7 @@
|
||||
{% block content %}
|
||||
<div class="grid place-content-center h-120 mt-12 mb-12 ">
|
||||
<div class="w-80 p-4 m-2 border-blue-600 border-2 rounded-xl shadow-md lg:w-90">
|
||||
<h2 class="text-2xl text-center p-4 font-black dark:text-white">Anmeldung</h2>
|
||||
<h2 class="text-2xl text-center p-4 font-black">Anmeldung</h2>
|
||||
<form class="space-y-4" method="post">
|
||||
{% if form.errors %}
|
||||
<div class=" items-center text-red-600 font-black overflow-x-auto break-words text-center">
|
||||
@@ -24,10 +24,9 @@
|
||||
<div class="items-center w-full">
|
||||
<input class=" w-full p-3 rounded-full bg-gray-200 focus:scale-105 transition duration-200 font-black" type="password" name="password" id="password" required placeholder="PASSWORT">
|
||||
</div>
|
||||
<button class="w-full p-3 rounded-full bg-blue-600 text-white font-black hover:scale-105 transition duration-200 cursor-pointer" type="submit" class="login-button">Anmelden</button>
|
||||
<button class="w-full p-3 rounded-full bg-blue-600 text-white font-black hover:scale-105 transition duration-200" type="submit" class="login-button">Anmelden</button>
|
||||
</form>
|
||||
<button class="text-center text-sm w-full mt-2 font-light text-blue-600"><a href="{% url 'accounts:register' %}" class="register_link">Noch kein Konto?</a></button>
|
||||
<button class="text-center text-sm w-full mt-2 font-light text-blue-600"><a href="{% url 'accounts:password_reset' %}" class="register_link">Passwort vergessen?</a></button>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="grid place-content-center h-120 mt-12 mb-12">
|
||||
|
||||
<div class="w-80 p-4 m-2 border-blue-600 border-2 rounded-xl shadow-md lg:w-90">
|
||||
<h2 class="text-2xl text-center p-4 font-black dark:text-white">Registrieren</h2>
|
||||
<h2 class="text-2xl text-center p-4 font-black">Registrieren</h2>
|
||||
|
||||
<form class=" space-y-4" method="post">
|
||||
{% if form.errors %}
|
||||
@@ -37,14 +37,7 @@
|
||||
{{ form.password2 }}
|
||||
|
||||
</div>
|
||||
<div class="register">
|
||||
|
||||
{{ form.email }}
|
||||
|
||||
</div>
|
||||
<div class="text-center text-gray-600 text-sm dislay-flex align-bottom">
|
||||
<label class=''>Mit der Erstellung ihres Kontos stimmen Sie der <a class="text-blue-600" href="/privacy">Datenschutzerklärung </a> zu.</label>
|
||||
</div>
|
||||
<button class="w-full p-3 rounded-full bg-blue-600 text-white font-black hover:scale-105 transition duration-200" type="submit" class="login-button">Registrieren</button>
|
||||
</form>
|
||||
<button class="text-center text-sm w-full mt-2 font-light text-blue-600"><a href="{% url 'accounts:login' %}" class="register_link">Bereits ein Konto?</a></button>
|
||||
17
core/templates/base.html
Normal file
17
core/templates/base.html
Normal file
@@ -0,0 +1,17 @@
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<link rel="icon" type="image/png" href="{% static 'icons/favicon.png' %}">
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="{% static 'css/t-style.css' %}">
|
||||
<title>qivip</title>
|
||||
</head>
|
||||
<body>
|
||||
{% include 'partials/_nav.html' %}
|
||||
{% block content%}{% endblock %}
|
||||
{% include 'partials/_footer.html' %}
|
||||
{% block extra_js %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
27
core/templates/chat/index.html
Normal file
27
core/templates/chat/index.html
Normal file
@@ -0,0 +1,27 @@
|
||||
<!-- chat/templates/chat/index.html -->
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<title>Chat Rooms</title>
|
||||
</head>
|
||||
<body>
|
||||
What chat room would you like to enter?<br>
|
||||
<input id="room-name-input" type="text" size="100"><br>
|
||||
<input id="room-name-submit" type="button" value="Enter">
|
||||
|
||||
<script>
|
||||
document.querySelector('#room-name-input').focus();
|
||||
document.querySelector('#room-name-input').onkeyup = function(e) {
|
||||
if (e.key === 'Enter') { // enter, return
|
||||
document.querySelector('#room-name-submit').click();
|
||||
}
|
||||
};
|
||||
|
||||
document.querySelector('#room-name-submit').onclick = function(e) {
|
||||
var roomName = document.querySelector('#room-name-input').value;
|
||||
window.location.pathname = '/chat/' + roomName + '/';
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
50
core/templates/chat/room.html
Normal file
50
core/templates/chat/room.html
Normal file
@@ -0,0 +1,50 @@
|
||||
<!-- chat/templates/chat/room.html -->
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<title>Chat Room</title>
|
||||
</head>
|
||||
<body>
|
||||
<textarea id="chat-log" cols="100" rows="20"></textarea><br>
|
||||
<input id="chat-message-input" type="text" size="100"><br>
|
||||
<input id="chat-message-submit" type="button" value="Send">
|
||||
{{ room_name|json_script:"room-name" }}
|
||||
<script>
|
||||
const roomName = JSON.parse(document.getElementById('room-name').textContent);
|
||||
|
||||
const chatSocket = new WebSocket(
|
||||
'ws://'
|
||||
+ window.location.host
|
||||
+ '/ws/chat/'
|
||||
+ roomName
|
||||
+ '/'
|
||||
);
|
||||
|
||||
chatSocket.onmessage = function(e) {
|
||||
const data = JSON.parse(e.data);
|
||||
document.querySelector('#chat-log').value += (data.message + '\n');
|
||||
};
|
||||
|
||||
chatSocket.onclose = function(e) {
|
||||
console.error('Chat socket closed unexpectedly');
|
||||
};
|
||||
|
||||
document.querySelector('#chat-message-input').focus();
|
||||
document.querySelector('#chat-message-input').onkeyup = function(e) {
|
||||
if (e.key === 'Enter') { // enter, return
|
||||
document.querySelector('#chat-message-submit').click();
|
||||
}
|
||||
};
|
||||
|
||||
document.querySelector('#chat-message-submit').onclick = function(e) {
|
||||
const messageInputDom = document.querySelector('#chat-message-input');
|
||||
const message = messageInputDom.value;
|
||||
chatSocket.send(JSON.stringify({
|
||||
'message': message
|
||||
}));
|
||||
messageInputDom.value = '';
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
22
core/templates/components/answer.html
Normal file
22
core/templates/components/answer.html
Normal file
@@ -0,0 +1,22 @@
|
||||
{% extends 'components/base.html' %}
|
||||
{% load static %}
|
||||
{% block title %}Antwort{% endblock %}
|
||||
{% block content %}
|
||||
<link rel="stylesheet" href="{% static 'homepage/t-style.css' %}">
|
||||
|
||||
<!-- TODO Bedingung festlegen: Quiztyp unterscheiden % if <Bedingung> % -->
|
||||
{% if user.is_authenticated %}
|
||||
<button class="overflow-x-auto text-[clamp(0.3rem,5vw,1rem)] break-words border-4 border-gray-500 text-2xl font-black m-0.75 bg-red-500 rounded-sm shadow-[0_2px_2px_rgba(0,0,0,0.1)] text-center text-white">1</button>
|
||||
<button class="overflow-x-auto text-[clamp(0.3rem,5vw,1rem)] break-words border-4 border-gray-500 text-2xl font-black m-0.75 bg-indigo-500 rounded-sm shadow-[0_2px_2px_rgba(0,0,0,0.1)] text-center text-white">2</button>
|
||||
<button class="overflow-x-auto text-[clamp(0.3rem,5vw,1rem)] break-words border-4 border-gray-500 text-2xl font-black m-0.75 bg-yellow-500 rounded-sm shadow-[0_2px_2px_rgba(0,0,0,0.1)] text-center text-white">3</button>
|
||||
<button class="overflow-x-auto text-[clamp(0.3rem,5vw,1rem)] break-words border-4 border-gray-500 text-2xl font-black m-0.75 bg-green-500 rounded-sm shadow-[0_2px_2px_rgba(0,0,0,0.1)] text-center text-white">4</button>
|
||||
|
||||
<!-- TODO wieder einfügen, wenn Bedingung gestetzt -->
|
||||
{% else %}
|
||||
<button class=" overflow-x-auto text-[clamp(0.3rem,5vw,1rem)] break-words border-4 border-gray-500 text-2xl font-black m-0.75 bg-green-500 rounded-sm shadow-[0_2px_2px_rgba(0,0,0,0.1)] text-center text-white">wahr</button>
|
||||
<button class=" overflow-x-auto text-[clamp(0.3rem,5vw,1rem)] break-words border-4 border-gray-500 text-2xl font-black m-0.75 bg-red-500 rounded-sm shadow-[0_2px_2px_rgba(0,0,0,0.1)] text-center text-white">falsch</button>
|
||||
|
||||
|
||||
<!--TODO wieder einfügen, wenn Bedingung gestetzt plus -->
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
45
core/templates/components/base.html
Normal file
45
core/templates/components/base.html
Normal file
@@ -0,0 +1,45 @@
|
||||
{% load static %}
|
||||
<!doctype html>
|
||||
<html class="h-full">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
</head>
|
||||
<body class="dark:bg-gray-900 h-full m-0">
|
||||
|
||||
<div style="height: calc(12vh);" class=" m-0 text-white bg-blue-600 rounded-lg h-12 shadow-md font-semibold px-1 w-full h-20 shadow-[0_2px_2px_rgba(0,0,0,0.1)] break-words text-center flex items-center justify-center overflow-x-auto text-[clamp(0.55rem,5vw,1.5rem)]" > Frage: Wie hoch ist der Eiffelturm insgesamt?
|
||||
</div>
|
||||
|
||||
<div class=" dark:bg-gray-900 flex justify-end gap-2 " style="height: calc(12vh);">
|
||||
<span class="mt-4 text-[clamp(0.6rem,5vw,1rem)] overflow-x-auto ml-0.75 text-white bg-blue-400 mb-6 h-12 rounded-sm p-3 shadow-[0_2px_2px_rgba(0,0,0,0.1)] z-40 items-center">Zeit: 60s</span>
|
||||
<div class="mt-4 text-[clamp(0.6rem,5vw,1rem)] overflow-x-auto mr-0.75 bg-gray-500 text-white h-12 mb-6 rounded-sm p-3 shadow-[0_2px_2px_rgba(0,0,0,0.1)] z-40 items-center ">Deine Punkte: 60</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="mt-3 flex justify-center -z-1 " style="height: calc(37vh);"> <img class="object-cover " src="{% static 'components/Ausgabe.png' %}" alt="Bild"> </div>
|
||||
<div class="w-full grid grid-cols-2 p-2 h-full" style="height: calc(38vh);min-height: 50px;">
|
||||
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="absolute bottom-0 right-0">
|
||||
<span class="text-[clamp(0.6rem,5vw,1rem)] font-black overflow-x-auto mt-1 ml-0.75 text-blue-600 p-0.75 bg-white">qivip
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
63
core/templates/components/ranking.html
Normal file
63
core/templates/components/ranking.html
Normal file
@@ -0,0 +1,63 @@
|
||||
{% load static %}
|
||||
<link rel="stylesheet" href="{% static 'homepage/t-style.css' %}">
|
||||
<!doctype html>
|
||||
<html class="h-full">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
</head>
|
||||
<body class=" dark:bg-gray-900 w-full justify-center h-full m-0">
|
||||
<div style="height: calc(10vh);" class=" m-0 text-white bg-blue-600 rounded-lg h-12 shadow-md font-semibold px-1 w-full h-20 shadow-[0_2px_2px_rgba(0,0,0,0.1)] break-words text-center flex items-center justify-center overflow-x-auto text-[clamp(0.75rem,5vw,1.5rem)]" >Ranking
|
||||
</div>
|
||||
<div class="m-4 flex justify-end font-bold">Frage 1/10</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="text-[clamp(0.75rem,5vw,1.25rem)] font-bold flex items-center justify-around border-blue-600 border-4 space-x-2 mt-2 overflow-x-auto rounded-lg p-4 shadow-[0_2px_2px_rgba(0,0,0,0.1)] w-full">
|
||||
<div style="background:linear-gradient(120deg, rgb(255, 208, 0),rgb(216, 151, 21))" class="text-white !important px-4 rounded-sm">1. Platz</div>
|
||||
<div class="text-stone-900 ">6000 Punkte</div>
|
||||
<div class="">Person XY</div>
|
||||
</div>
|
||||
|
||||
<div class=" text-[clamp(0.75rem,5vw,1.25rem)] font-bold p-4 flex items-center justify-around border-blue-600 border-4 space-x-2 mt-2 overflow-x-auto rounded-lg shadow-[0_2px_2px_rgba(0,0,0,0.1)] w-full">
|
||||
<div style="background:linear-gradient(120deg, rgb(102, 122, 122),rgb(194, 186, 186))" class="text-white !important px-4 rounded-sm">2. Platz</div>
|
||||
<div class="text-stone-900 ">5000 Punkte</div>
|
||||
<div class="">Person XY</div>
|
||||
</div>
|
||||
|
||||
<div class=" text-[clamp(0.75rem,5vw,1.25rem)] font-bold flex items-center justify-around border-blue-600 border-4 space-x-2 mt-2 overflow-x-auto rounded-lg p-4 shadow-[0_2px_2px_rgba(0,0,0,0.1)] w-full">
|
||||
<div style="background:linear-gradient(120deg, rgb(211, 91, 17),rgb(219, 168, 109))" class="text-white !important px-4 rounded-sm">3. Platz</div>
|
||||
<div class="text-stone-900 ">600 Punkte</div>
|
||||
<div class="">Person XY</div>
|
||||
</div>
|
||||
|
||||
<div class=" text-[clamp(0.75rem,5vw,1.25rem)] items-center flex justify-around font-bold border-blue-600 border-4 space-x-2 mt-2 text-[clamp(0.6rem,5vw,1rem)] overflow-x-auto rounded-lg p-4 shadow-[0_2px_2px_rgba(0,0,0,0.1)] w-full">
|
||||
<div>4. Platz</div>
|
||||
<div>200 Punkte</div>
|
||||
<div>Person XY</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="absolute bottom-0 right-0">
|
||||
<span class="text-[clamp(0.6rem,5vw,1rem)] font-black overflow-x-auto mt-1 ml-0.75 text-blue-600 p-0.75 bg-white">qivip
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
15
core/templates/homepage/home.html
Normal file
15
core/templates/homepage/home.html
Normal file
@@ -0,0 +1,15 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="grid place-content-center md:h-screen h-150 w-full -z-50 top-0 p-4">
|
||||
<div class="text-center">
|
||||
<h2 class="font-bold md:text-8xl text-6xl md:mb-8 text-blue-600">qivip</h2>
|
||||
<h3 class="italic font-extralight sm:text-2xl text-md">Interaktives Lernen neu definiert.</h3>
|
||||
</div>
|
||||
<div class="grid sm:grid-cols-3 place-items-stretch text-center mt-8 gap-4 p-4 border-3 bg-blue-100 border-blue-100 rounded-md">
|
||||
<a class="qp-a-button bg-green-500 text-white" href="#">Teilnehmen</a>
|
||||
<a class="qp-a-button bg-indigo-500 text-white" href="#">Moderieren</a>
|
||||
<a class="qp-a-button bg-purple-500 text-white" href="#">Verwalten</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
22
core/templates/homepage/impress.html
Normal file
22
core/templates/homepage/impress.html
Normal file
@@ -0,0 +1,22 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block content%}
|
||||
<div>
|
||||
<div>
|
||||
<h1>Impressum</h1>
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
<p>
|
||||
Katharineum zu Lübeck
|
||||
</p>
|
||||
<p>
|
||||
Königsstraße 27-31
|
||||
</p>
|
||||
<p>
|
||||
23552 Lübeck
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
13
core/templates/homepage/privacy.html
Normal file
13
core/templates/homepage/privacy.html
Normal file
@@ -0,0 +1,13 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content%}
|
||||
<div>
|
||||
<p>
|
||||
Hallo, ich bin die Datenschutzerklärung!
|
||||
</p>
|
||||
</div
|
||||
{% endblock %}
|
||||
>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -3,15 +3,15 @@
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="max-w-lg mx-auto mt-10">
|
||||
<div class="bg-white rounded-lg shadow-md p-6 border-blue-600 border-2 rounded-xl shadow-md dark:text-white dark:bg-transparent">
|
||||
<div class="bg-white rounded-lg shadow-md p-6 border-blue-600 border-2 rounded-xl shadow-md">
|
||||
<div class="text-center">
|
||||
<svg class="mx-auto h-12 w-12 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
|
||||
</svg>
|
||||
|
||||
<h2 class="mt-4 text-xl font-bold text-gray-900 dark:text-white">Löschen bestätigen</h2>
|
||||
<h2 class="mt-4 text-xl font-bold text-gray-900">Löschen bestätigen</h2>
|
||||
|
||||
<p class="mt-2 text-sm text-gray-600 dark:text-white">
|
||||
<p class="mt-2 text-sm text-gray-600">
|
||||
{% if object.quiz_id %}
|
||||
Möchten Sie die Frage "{{ object.data.question }}" wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.
|
||||
{% else %}
|
||||
114
core/templates/library/detail_quiz.html
Normal file
114
core/templates/library/detail_quiz.html
Normal file
@@ -0,0 +1,114 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold">{{ quiz.name }}</h1>
|
||||
<div class="flex space-x-2">
|
||||
<a href="{% url 'library:edit_quiz' quiz.id %}" class="bg-green-500 text-white px-4 py-2 rounded-md text-sm lg:text-lg hover:bg-green-600 transition-colors">Quiz bearbeiten</a>
|
||||
<a href="{% url 'library:delete_quiz' quiz.id %}" class="bg-red-500 text-white px-4 py-2 rounded-md text-sm lg:text-lg hover:bg-red-600 transition-colors">Quiz löschen</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if quiz.description %}
|
||||
<p class="text-gray-600 mb-8">{{ quiz.description }}</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="bg-white rounded-lg shadow-md border-blue-600 border-2 rounded-xl shadow-md">
|
||||
<div class="border-b border-gray-200 p-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="text-xl font-semibold">Fragen</h2>
|
||||
<div class="flex space-x-2">
|
||||
<a href="{% url 'library:new_question' %}?type=multiple_choice&quiz_id={{ quiz.id }}"
|
||||
class="bg-blue-100 text-blue-800 px-4 py-2 rounded-md text-xs lg:text-lg hover:border-blue-600 border-2">
|
||||
Multiple Choice
|
||||
</a>
|
||||
<a href="{% url 'library:new_question' %}?type=true_false&quiz_id={{ quiz.id }}"
|
||||
class="bg-purple-100 text-purple-800 px-4 py-2 rounded-md text-xs lg:text-lg hover:border-blue-600 border-2">
|
||||
Wahr/Falsch
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if questions %}
|
||||
|
||||
<div class="divide-y divide-gray-200">
|
||||
{% for question in questions %}
|
||||
|
||||
<div class="w-full rounded-xl p-4 hover:bg-gray-50">
|
||||
<a class="block flex h-full w-full" href="{% url 'library:edit_question' question.id %}" >
|
||||
<div class="flex justify-between items-start">
|
||||
|
||||
<div class="flex-grow">
|
||||
<p class="font-medium">{{ question.data.question }}</p>
|
||||
<div class="mt-1">
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium {% if question.data.type == 'multiple_choice' %}bg-blue-100 text-blue-800{% else %}bg-purple-100 text-purple-800{% endif %}">
|
||||
{% if question.data.type == 'multiple_choice' %}Multiple Choice{% else %}Wahr/Falsch{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
{% if question.data.type == 'multiple_choice' %}
|
||||
<ul class="space-y-1">
|
||||
{% for option in question.data.options %}
|
||||
<li class="flex items-center text-sm">
|
||||
{% if option.is_correct %}
|
||||
<svg class="h-4 w-4 text-green-500 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
<span class="font-medium text-green-700">{{ option.value }}</span>
|
||||
{% else %}
|
||||
<svg class="h-4 w-4 text-gray-400 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 12H6"/>
|
||||
</svg>
|
||||
<span class="text-gray-600">{{ option.value }}</span>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
{% for option in question.data.options %}
|
||||
{% if option.is_correct %}
|
||||
<p class="text-sm">
|
||||
<span class="text-gray-500">Richtige Antwort:</span>
|
||||
<span class="ml-1 font-medium {% if option.value == "Wahr" %} text-green-700 {% else %}text-red-700{% endif %}">{{ option.value }}</span>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex h-full space-x-2 ml-4 items-center">
|
||||
<!--
|
||||
<a href="{% url 'library:edit_question' question.id %}"
|
||||
class="bg-gray-100 text-gray-700 px-3 py-1 rounded hover:bg-gray-200 transition-colors">
|
||||
Bearbeiten
|
||||
</a>
|
||||
-->
|
||||
|
||||
<a href=" {% url 'library:delete_question' question.id %}"
|
||||
class=" text-red-700 px-4 py-1 rounded hover:scale-110 ">
|
||||
🗑
|
||||
</a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
|
||||
<div class="p-8 text-center">
|
||||
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/>
|
||||
</svg>
|
||||
<p class="mt-2 text-sm text-gray-500">Noch keine Fragen vorhanden. Erstellen Sie eine neue Frage, um loszulegen!</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
13
core/templates/library/form.html
Normal file
13
core/templates/library/form.html
Normal file
@@ -0,0 +1,13 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Formular</h1>
|
||||
<h2>{{ form.name.value }}</h2>
|
||||
<div class="input-group border-blue-600 border-2 rounded-xl shadow-md">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<button type="submit">Speichern</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
28
core/templates/library/overview_quiz.html
Normal file
28
core/templates/library/overview_quiz.html
Normal file
@@ -0,0 +1,28 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Übersicht</h1>
|
||||
<div class="flex justify-end">
|
||||
<a href="{% url 'library:new_quiz' %}" class="text-white bg-blue-500 px-4 py-2 rounded-md mb-12">Neues Quiz erstellen</a>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 px-4 place-items-center">
|
||||
{% for quiz in quizzes %}
|
||||
<div class="bg-white w-full max-w-md rounded-lg p-4 shadow-md border-blue-600 border-2 rounded-xl">
|
||||
<h2 class="text-lg font-bold"><a href="{% url 'library:edit_quiz' quiz.id %}">{{ quiz.name }}</a></h2>
|
||||
<p class="text-sm text-gray-600 py-12">{{ quiz.description }}</p>
|
||||
|
||||
<div>
|
||||
<div class="flex justify-between items-center gap-2">
|
||||
<a href="#" class="qp-a-button-small border-2 border-blue-600 text-black">Spiel starten</a>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<a href="{% url 'library:detail_quiz' quiz.id %}" class="qp-a-button-small border-2 border-blue-600 text-white">✏</a>
|
||||
<a href="{% url 'library:delete_quiz' quiz.id %}" class="qp-a-button-small border-2 border-blue-600 text-white">🗑</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
164
core/templates/library/question/question_multiple_choice.html
Normal file
164
core/templates/library/question/question_multiple_choice.html
Normal file
@@ -0,0 +1,164 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold">{% if question %}Frage bearbeiten{% else %}Neue Multiple Choice Frage{% endif %}</h1>
|
||||
<a href="{% url 'library:detail_quiz' quiz.id %}"
|
||||
class="bg-gray-100 text-gray-700 px-4 py-2 rounded-md hover:bg-gray-200 transition-colors">
|
||||
Zurück zum Quiz
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-lg shadow-md p-6">
|
||||
<form method="post" class="space-y-6">
|
||||
{% csrf_token %}
|
||||
|
||||
<!-- Question Text -->
|
||||
<div>
|
||||
<label for="question" class="block text-sm font-medium text-gray-700">Frage</label>
|
||||
<div class="mt-1">
|
||||
<textarea name="question" id="question" rows="4"
|
||||
class="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
|
||||
required>{{ question.data.question|default:'' }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Options -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Antwortmöglichkeiten</label>
|
||||
<div id="options-container" class="space-y-3">
|
||||
{% if question and question.data.options %}
|
||||
{% for option in question.data.options %}
|
||||
<div class="flex items-center space-x-3">
|
||||
<textarea name="option_{{ forloop.counter }}" rows="2"
|
||||
class="shadow-sm focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
|
||||
required>{{ option.value }}</textarea>
|
||||
<label class="inline-flex items-center">
|
||||
<input type="checkbox" name="correct_{{ forloop.counter }}" value="1"
|
||||
class="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded"
|
||||
{% if option.is_correct %}checked{% endif %}>
|
||||
<span class="ml-2 text-sm text-gray-600">Richtig</span>
|
||||
</label>
|
||||
<button type="button" onclick="removeOption(this)"
|
||||
class="text-red-600 hover:text-red-800 transition-colors">
|
||||
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="flex items-center space-x-3">
|
||||
<textarea name="option_1" rows="2"
|
||||
class="shadow-sm focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
|
||||
required></textarea>
|
||||
<label class="inline-flex items-center">
|
||||
<input type="checkbox" name="correct_1" value="1"
|
||||
class="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded">
|
||||
<span class="ml-2 text-sm text-gray-600">Richtig</span>
|
||||
</label>
|
||||
<button type="button" onclick="removeOption(this)"
|
||||
class="text-red-600 hover:text-red-800 transition-colors">
|
||||
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center space-x-3">
|
||||
<textarea name="option_2" rows="2"
|
||||
class="shadow-sm focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
|
||||
required></textarea>
|
||||
<label class="inline-flex items-center">
|
||||
<input type="checkbox" name="correct_2" value="1"
|
||||
class="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded">
|
||||
<span class="ml-2 text-sm text-gray-600">Richtig</span>
|
||||
</label>
|
||||
<button type="button" onclick="removeOption(this)"
|
||||
class="text-red-600 hover:text-red-800 transition-colors">
|
||||
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<button type="button" onclick="addOption()"
|
||||
class="mt-3 inline-flex items-center px-3 py-1.5 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
<svg class="h-4 w-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
|
||||
</svg>
|
||||
Option hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end space-x-3">
|
||||
<a href="{% url 'library:detail_quiz' quiz.id %}"
|
||||
class="inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
Abbrechen
|
||||
</a>
|
||||
<button type="submit"
|
||||
class="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
{% if question %}Speichern{% else %}Erstellen{% endif %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function addOption() {
|
||||
const container = document.getElementById('options-container');
|
||||
const optionCount = container.children.length + 1;
|
||||
|
||||
if (optionCount > 6) {
|
||||
alert('Maximal 6 Optionen sind erlaubt.');
|
||||
return;
|
||||
}
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.className = 'flex items-center space-x-3';
|
||||
div.innerHTML = `
|
||||
<textarea name="option_${optionCount}" rows="2"
|
||||
class="shadow-sm focus:ring-blue-500 focus:border-blue-500 flex-grow sm:text-sm border-gray-300 rounded-md"
|
||||
required></textarea>
|
||||
<label class="inline-flex items-center">
|
||||
<input type="checkbox" name="correct_${optionCount}" value="1"
|
||||
class="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded">
|
||||
<span class="ml-2 text-sm text-gray-600">Richtig</span>
|
||||
</label>
|
||||
<button type="button" onclick="removeOption(this)"
|
||||
class="text-red-600 hover:text-red-800 transition-colors">
|
||||
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
||||
</svg>
|
||||
</button>
|
||||
`;
|
||||
|
||||
container.appendChild(div);
|
||||
}
|
||||
|
||||
function removeOption(button) {
|
||||
const container = document.getElementById('options-container');
|
||||
if (container.children.length > 2) {
|
||||
button.parentElement.remove();
|
||||
updateOptionNumbers();
|
||||
} else {
|
||||
alert('Mindestens zwei Optionen müssen vorhanden sein.');
|
||||
}
|
||||
}
|
||||
|
||||
function updateOptionNumbers() {
|
||||
const container = document.getElementById('options-container');
|
||||
const options = container.children;
|
||||
Array.from(options).forEach((option, index) => {
|
||||
const number = index + 1;
|
||||
const textInput = option.querySelector('textarea');
|
||||
const checkbox = option.querySelector('input[type="checkbox"]');
|
||||
|
||||
textInput.name = `option_${number}`;
|
||||
checkbox.name = `correct_${number}`;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
59
core/templates/library/question/question_true_false.html
Normal file
59
core/templates/library/question/question_true_false.html
Normal file
@@ -0,0 +1,59 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold">{% if question %}Frage bearbeiten{% else %}Neue Wahr/Falsch Frage{% endif %}</h1>
|
||||
<a href="{% url 'library:detail_quiz' quiz.id %}"
|
||||
class="bg-gray-100 text-gray-700 px-4 py-2 rounded-md hover:bg-gray-200 transition-colors">
|
||||
Zurück zum Quiz
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-lg shadow-md p-6">
|
||||
<form method="post" class="space-y-6">
|
||||
{% csrf_token %}
|
||||
|
||||
<!-- Question Text -->
|
||||
<div>
|
||||
<label for="question" class="block text-sm font-medium text-gray-700">Frage</label>
|
||||
<div class="mt-1">
|
||||
<textarea name="question" id="question" rows="4"
|
||||
class="shadow-sm focus:ring-blue-500 focus:border-blue-500 block w-full sm:text-sm border-gray-300 rounded-md"
|
||||
required>{{ question.data.question }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- True/False Selection -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Richtige Antwort</label>
|
||||
<div class="flex space-x-4">
|
||||
<label class="inline-flex items-center">
|
||||
<input type="radio" name="correct_answer" value="true"
|
||||
class="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300"
|
||||
{% if question.data.correct_answer %}checked{% endif %}>
|
||||
<span class="ml-2 text-sm text-gray-600">Wahr</span>
|
||||
</label>
|
||||
<label class="inline-flex items-center">
|
||||
<input type="radio" name="correct_answer" value="false"
|
||||
class="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300"
|
||||
{% if not question.data.correct_answer %}checked{% endif %}>
|
||||
<span class="ml-2 text-sm text-gray-600">Falsch</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end space-x-3">
|
||||
<a href="{% url 'library:detail_quiz' quiz.id %}"
|
||||
class="inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
Abbrechen
|
||||
</a>
|
||||
<button type="submit"
|
||||
class="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
{% if question %}Speichern{% else %}Erstellen{% endif %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
7
core/templates/partials/_footer.html
Normal file
7
core/templates/partials/_footer.html
Normal file
@@ -0,0 +1,7 @@
|
||||
<div class="grid place-content-center gap-2 w-full mt-4">
|
||||
<div class="flex space-x-2 pb-4">
|
||||
<p class="text-sm font-extralight"><a href="/impress">Impressum</a></p>
|
||||
<p class="text-sm font-extralight"><a href="/privacy">Datenschutz</a></p>
|
||||
<p class="text-sm font-extralight"><a href="https://edugit.org/enrichment-2024/qivip">Repository</a></p>
|
||||
</div>
|
||||
</div>
|
||||
15
core/templates/partials/_nav.html
Normal file
15
core/templates/partials/_nav.html
Normal file
@@ -0,0 +1,15 @@
|
||||
<nav class="flex justify-between bg-blue-600 h-12 px-4 sm:px-6 lg:px-8 rounded-lg m-2 shadow-md">
|
||||
<div class="flex items-center">
|
||||
<h2 class="text-xl font-bold text-white hover:scale-110 transition duration-200"><a href="{% url 'homepage:home' %}">qivip</a></h2>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<ul class="flex space-x-4 qp-nav-list">
|
||||
<li><a href="{% url 'library:overview_quiz' %}">Bibliothek</a></li>
|
||||
{% if user.is_authenticated %}
|
||||
<li><a href="{% url 'accounts:home' %}">Konto</a></li>
|
||||
{% else %}
|
||||
<li><a href="{% url 'accounts:login' %}">Anmelden</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
0
core/templates/play/game/score_overview.html
Normal file
0
core/templates/play/game/score_overview.html
Normal file
@@ -1,13 +1,13 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="sm:grid sm:place-items-center sm:min-h-[calc(100vh-5rem)] flex justify-center py-6 m-2">
|
||||
<div class="max-w-md w-full p-8 rounded-2xl border-2 border-blue-400 bg-white dark:bg-transparent dark:text-white">
|
||||
<div class="grid place-items-center min-h-[calc(100vh-5rem)] py-6 m-2">
|
||||
<div class="max-w-md w-full p-8 rounded-2xl border-2 border-blue-400 bg-white">
|
||||
<h1 class="text-2xl text-center font-bold mb-4">Spiel beitreten</h1>
|
||||
<form action="{% url 'play:join_game' %}" method="post" class="flex flex-col gap-4">
|
||||
{% csrf_token %}
|
||||
<div class="flex gap-3 items-center justify-center">
|
||||
<input type="number" name="game_code" placeholder="123456" maxlength="6" class="w-32 p-3 rounded-full bg-gray-200 focus:scale-105 transition duration-200 font-black focus:outline-none focus:ring-2 focus:ring-blue-400 focus:bg-blue-100 text-center tracking-wider text-black" autofocus>
|
||||
<input type="text" name="game_code" placeholder="XXX-XXX" maxlength="7" pattern="[A-Za-z0-9]{3}-[A-Za-z0-9]{3}" class="w-32 p-3 rounded-full bg-gray-200 focus:scale-105 transition duration-200 font-black focus:outline-none focus:ring-2 focus:ring-blue-400 focus:bg-blue-100 text-center uppercase tracking-wider" oninput="let v = this.value.replace(/[^A-Za-z0-9]/g, '').slice(0,6).toUpperCase(); this.value = v.length > 3 ? v.slice(0,3) + '-' + v.slice(3) : v;" autofocus>
|
||||
<button type="submit" class="h-12 px-6 rounded-full bg-blue-600 text-white font-black hover:scale-105 transition duration-200 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed hover:bg-blue-700 active:scale-95 group" disabled>
|
||||
<span class="hidden sm:block">Beitreten</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5 transition-transform group-hover:translate-x-1">
|
||||
@@ -15,20 +15,13 @@
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{% if messages %}
|
||||
<ul class="messages">
|
||||
{% for message in messages %}
|
||||
<li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</form>
|
||||
<script>
|
||||
const input = document.querySelector('input[name="game_code"]');
|
||||
const button = document.querySelector('button[type="submit"]');
|
||||
|
||||
input.addEventListener('input', function(e) {
|
||||
button.disabled = !this.value.match(/^[0-9]{6}$/);
|
||||
button.disabled = !this.value.match(/^[A-Z0-9]{3}-[A-Z0-9]{3}$/);
|
||||
});
|
||||
|
||||
input.addEventListener('keypress', function(e) {
|
||||
15
core/templates/play/lobby.html
Normal file
15
core/templates/play/lobby.html
Normal file
@@ -0,0 +1,15 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4">
|
||||
<h1>Bundeslaender Deutschland Quiz</h1>
|
||||
<div id="player-list">
|
||||
<ul>
|
||||
<li>Spieler 1</li>
|
||||
<li>Spieler 2</li>
|
||||
<li>Spieler 3</li>
|
||||
</ul>
|
||||
</div>
|
||||
<button class="w-full p-3 rounded-full bg-blue-600 text-white font-black hover:scale-105 transition duration-200" type="submit">Starten</button>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,7 +0,0 @@
|
||||
Name und Anschrift des Verantwortlichen
|
||||
|
||||
//NAME HIER EINSETZEN//
|
||||
|
||||
//ADRESSE HIER EINSETZEN//
|
||||
|
||||
//E-MAIL HIER EINSETZEN//
|
||||
@@ -1,35 +0,0 @@
|
||||
from django.urls import path, reverse_lazy # type: ignore
|
||||
from . import views
|
||||
from django.contrib.auth.views import LogoutView
|
||||
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
|
||||
#
|
||||
# Wichtig: Damit Links funktionieren müssen diese so eingebunden werden: {% url 'accounts:login' %} statt {% url 'login' %}
|
||||
#
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.home, name='home'),
|
||||
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('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'
|
||||
),
|
||||
|
||||
]
|
||||
@@ -1,65 +0,0 @@
|
||||
from django.shortcuts import render, redirect
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth import login, authenticate
|
||||
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
|
||||
|
||||
|
||||
# Create your views here.
|
||||
@login_required
|
||||
def home(request):
|
||||
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 = {
|
||||
'my_quizzes_number': my_quizzes_number,
|
||||
'my_questions_number': my_questions_number,
|
||||
}
|
||||
return render(request, 'accounts/home.html', context=context)
|
||||
|
||||
|
||||
|
||||
def register(response):
|
||||
if response.method == "POST":
|
||||
form = RegisterForm(response.POST)
|
||||
if form.is_valid():
|
||||
user=form.save()
|
||||
login(response, user) #automatischer Login nach Registrierung
|
||||
return redirect("accounts:home")
|
||||
else:
|
||||
form = RegisterForm()
|
||||
|
||||
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')
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user