Compare commits

..

4 Commits

Author SHA1 Message Date
9b13749a29 Change Allowed Hosts 2025-03-15 13:40:45 +01:00
Juhn-Vitus Saß
b6831616cc Channel Layers 2025-03-15 12:57:05 +01:00
889134784a Basestructure Chat App 2025-03-15 12:35:32 +01:00
ab6e27f80e Add chat App 2025-03-15 12:15:45 +01:00
160 changed files with 642 additions and 4861 deletions

3
.gitignore vendored
View File

@@ -4,6 +4,3 @@ db.sqlite3
tailwindcss.exe
t-style.css
dump.rdb
media
node_modules
staticfiles

View File

@@ -47,43 +47,17 @@ Um das Projekt erfolgreich zu starten, folge diesen Schritten:
### 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
```
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)
Damit ist das Projekt erfolgreich eingerichtet und der Server kann gestartet werden!
## Superuser erstellen
@@ -126,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

View File

@@ -1,6 +1,5 @@
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
# Create your views here.
@@ -12,9 +11,8 @@ 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")
form.save()
return redirect("home")
else:
form = RegisterForm()

40
core/chat/consumers.py Normal file
View 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
View 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
View 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
View 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})

34
core/core/asgi.py Normal file
View 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))
),
}
)

View File

@@ -25,29 +25,29 @@ 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
ALLOWED_HOSTS = ['127.0.0.1', 'localhost', '*']
ALLOWED_HOSTS = ['127.0.0.1', 'localhost', '*', '172.22.10.111']
# 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',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
@@ -75,13 +75,6 @@ TEMPLATES = [
]
WSGI_APPLICATION = 'core.wsgi.application'
ASGI_APPLICATION = 'core.asgi.application'
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels.layers.InMemoryChannelLayer'
}
}
# Database
@@ -94,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
@@ -139,22 +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.CompressedStaticFilesStorage'
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"
# Channels
ASGI_APPLICATION = "core.asgi.application"
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [("127.0.0.1", 8000)],
},
},
}

View File

@@ -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,8 +23,5 @@ urlpatterns = [
path('', include('homepage.urls')),
path('play/', include('play.urls')),
path('library/', include('library.urls')),
path('components/', include('components.urls'))
path('chat/', include('chat.urls')),
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View File

@@ -1,14 +1,15 @@
from django.contrib import admin
from .models import QivipQuiz, QivipQuestion, QuizCategory
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 = ('id', 'quiz_id', 'data', 'creation_date', 'update_date')
list_display = ('quiz_id', 'data', 'creation_date', 'update_date')
search_fields = ('data',)
list_filter = ('quiz_id',)
@@ -27,4 +28,4 @@ class TagAdmin(admin.ModelAdmin):
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
View 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']

65
core/library/models.py Normal file
View 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

View File

@@ -1,8 +1,6 @@
from django.urls import path
from django.conf import settings
from . import views
from django.conf.urls.static import static
app_name = 'library'
urlpatterns = [
@@ -14,6 +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'),
]

View File

@@ -1,4 +1,3 @@
import uuid
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
@@ -7,104 +6,24 @@ from django.contrib import messages
from .models import QivipQuiz, QivipQuestion
from .forms import QuizForm, QuestionForm
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
# Übersicht aller Quizze
@login_required
def overview_quiz(request):
# Filter
form = QuizFilterForm(request.GET)
# Initialize querysets
if request.user.is_authenticated:
filter_my_quizzes = QivipQuiz.objects.filter(user_id=request.user).annotate(max_amout_questions=Count('questions'))
filter_other_quizzes = QivipQuiz.objects.exclude(user_id=request.user)
else:
filter_my_quizzes = QivipQuiz.objects.none()
filter_other_quizzes = QivipQuiz.objects.all()
# Apply common filters to other quizzes
filter_other_quizzes = filter_other_quizzes.annotate(max_amout_questions=Count('questions'))
filter_other_quizzes = filter_other_quizzes.filter(max_amout_questions__gte=1, status='öffentlich')
if form.is_valid():
search = form.cleaned_data.get('search')
username= form.cleaned_data.get('user')
max_amout_questions= form.cleaned_data.get('max_amout_questions')
min_amout_questions= form.cleaned_data.get('min_amout_questions')
if search: # Suche nach Namen
filter_other_quizzes = filter_other_quizzes.filter(name__icontains=search)
filter_my_quizzes = filter_my_quizzes.filter(name__icontains=search)
if min_amout_questions:
filter_other_quizzes = filter_other_quizzes.filter(max_amout_questions__gte=min_amout_questions)
filter_my_quizzes = filter_my_quizzes.filter(max_amout_questions__gte=min_amout_questions) if filter_my_quizzes else QivipQuiz.objects.none()
if max_amout_questions:
filter_other_quizzes = filter_other_quizzes.filter(max_amout_questions__lte=max_amout_questions)
try:
filter_my_quizzes =filter_my_quizzes.filter(max_amout_questions__lte=max_amout_questions)
except:
filter_my_quizzes = QivipQuiz.objects.none()
if username:
try:
user = User.objects.get(username=username) # Benutzer anhand des Namens holen
filter_other_quizzes = filter_other_quizzes.filter(user_id=user.id)
filter_my_quizzes = filter_my_quizzes.filter(user_id=user.id) # Nach der user_id filtern
except User.DoesNotExist:
filter_other_quizzes = QivipQuiz.objects.none() # Falls User nicht existiert, leere Query zurückgeben
filter_my_quizzes = QivipQuiz.objects.none()
try:
filter_other_quizzes=filter_other_quizzes.exclude(user_id=request.user)
except:
filter_other_quizzes=filter_other_quizzes
# Pagination for my quizzes
my_quizzes_paginator = Paginator(filter_my_quizzes.order_by('-creation_date'), 8) # 8 quizzes per page
try:
my_quizzes = my_quizzes_paginator.page(request.GET.get('my_page', 1))
except:
my_quizzes = my_quizzes_paginator.page(1)
# Pagination for other quizzes
other_quizzes_paginator = Paginator(filter_other_quizzes.order_by('-creation_date'), 8) # 8 quizzes per page
try:
other_quizzes = other_quizzes_paginator.page(request.GET.get('other_page', 1))
except:
other_quizzes = other_quizzes_paginator.page(1)
context = {
'show_search': True,
'my_quizzes': my_quizzes,
'other_quizzes': other_quizzes,
'form': form,
}
return render(request, 'library/overview_quiz.html', context)
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, request.FILES)
form = QuizForm(request.POST)
if form.is_valid():
quiz = form.save(commit=False)
quiz.user_id = request.user
#quiz.creator = request.user
quiz.save()
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:
form = QuizForm()
return render(request, 'library/form.html', {'form': form})
@@ -114,13 +33,10 @@ def new_quiz(request):
def edit_quiz(request, pk):
quiz = get_object_or_404(QivipQuiz, pk=pk, user_id=request.user)
if request.method == 'POST':
form = QuizForm(request.POST, instance=quiz, files=request.FILES)
form = QuizForm(request.POST, instance=quiz)
if form.is_valid():
form.save()
return redirect('library:detail_quiz', pk=pk)
#return modified(request, pk)
return redirect('library:overview_quiz')
else:
form = QuizForm(instance=quiz)
return render(request, 'library/form.html', {'form': form})
@@ -135,11 +51,11 @@ def delete_quiz(request, pk):
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)
quiz = get_object_or_404(QivipQuiz, pk=pk)
show_answers = request.GET.get('show_answers', 'false').lower() == 'true'
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:
@@ -147,68 +63,9 @@ def detail_quiz(request, pk):
context = {
'quiz': quiz,
'questions': questions,
'detail':show_answers,
'questions': questions
}
return render(request, 'library/detail_quiz.html', context)
"""
def modified(request, pk):
original_quiz = get_object_or_404(QivipQuiz, pk=pk)
words=original_quiz.modified_description.split()
word_exist=0
print(original_quiz.creator.username)
print(request.user.username)
for word in words:
if word==request.user.username or word+","==request.user.username:
word_exist=1
if word_exist==0 and len(words)!=0 and request.user.username!=original_quiz.creator.username:
original_quiz.modified_description +=", "+ request.user.username
elif word_exist==0 and request.user.username!=original_quiz.creator.username:
original_quiz.modified_description +=" "+ request.user.username
original_quiz.save()
return redirect('library:detail_quiz', pk=pk)
"""
@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" # Optional: Name anpassen
new_quiz.base_quiz_id = pk
#new_quiz.creator = original_quiz.creator or request.user
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
@login_required
@@ -284,7 +141,6 @@ def new_question(request):
question.data = json.dumps(json_data)
question.quiz_id = quiz
question.save()
#return modified(request, pk=quiz.pk)
return redirect('library:detail_quiz', pk=quiz.pk)
else:
# Initialize empty question data for new questions
@@ -413,9 +269,7 @@ def edit_question(request, pk):
question.data = json.dumps(json_data)
question.save()
#return modified(request, question.quiz_id.pk)
return redirect('library:detail_quiz', pk=question.quiz_id.pk)
else:
template_name = f'library/question/question_{question_type}.html'
context = {
@@ -426,7 +280,6 @@ def edit_question(request, pk):
return render(request, template_name, context)
# Frage löschen
@login_required
def delete_question(request, pk):

0
core/play/__init__.py Normal file
View File

3
core/play/admin.py Normal file
View File

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

View File

3
core/play/models.py Normal file
View File

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

9
core/play/urls.py Normal file
View 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
View 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')

View 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;
}

View File

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

17
core/templates/base.html Normal file
View 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>

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

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

View File

@@ -2,7 +2,7 @@
{% load static %}
{% block title %}Antwort{% endblock %}
{% block content %}
<link rel="stylesheet" href="{% static 'css/t-style.css' %}">
<link rel="stylesheet" href="{% static 'homepage/t-style.css' %}">
<!-- TODO Bedingung festlegen: Quiztyp unterscheiden % if <Bedingung> % -->
{% if user.is_authenticated %}

View File

@@ -1,5 +1,5 @@
{% load static %}
<link rel="stylesheet" href="{% static 'css/t-style.css' %}">
<link rel="stylesheet" href="{% static 'homepage/t-style.css' %}">
<!doctype html>
<html class="h-full">
<head>

View 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 %}

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

View File

@@ -0,0 +1,13 @@
{% extends 'base.html' %}
{% block content%}
<div>
<p>
Hallo, ich bin die Datenschutzerklärung!
</p>
</div
{% endblock %}
>
</div>
</body>
</html>

View 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 ">
&#x1F5D1
</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 %}

View 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 %}

View 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">&#x270F;</a>
<a href="{% url 'library:delete_quiz' quiz.id %}" class="qp-a-button-small border-2 border-blue-600 text-white">&#x1F5D1;</a>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% endblock %}

View File

@@ -10,7 +10,7 @@
</a>
</div>
<div class="bg-white rounded-lg shadow-md p-6 border-blue-600 border-2 rounded-xl shadow-md">
<div class="bg-white rounded-lg shadow-md p-6">
<form method="post" class="space-y-6">
{% csrf_token %}

View File

@@ -10,7 +10,7 @@
</a>
</div>
<div class="bg-white rounded-lg shadow-md p-6 border-blue-600 border-2 rounded-xl shadow-md">
<div class="bg-white rounded-lg shadow-md p-6">
<form method="post" class="space-y-6">
{% csrf_token %}

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

View 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="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="text" name="game_code" placeholder="123456" maxlength="6" class="w-32 p-3 rounded-full bg-gray-200 focus:scale-105 transition duration-200 font-black focus:outline-none focus:ring-2 focus:ring-blue-400 focus:bg-blue-100 text-center tracking-wider" autofocus>
<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) {

View 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 %}

View File

@@ -1,28 +0,0 @@
"""
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 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')
django.setup()
from play.routing import websocket_urlpatterns
application = ProtocolTypeRouter({
'http': get_asgi_application(),
'websocket': AuthMiddlewareStack(
URLRouter(
websocket_urlpatterns
)
),
})

View File

@@ -1,31 +0,0 @@
from django import forms
from .models import QivipQuiz, QivipQuestion
class QuizForm(forms.ModelForm):
class Meta:
model = QivipQuiz
fields = ['name', 'description','image', 'status', 'category', "difficulty","credits"]
class QuestionForm(forms.ModelForm):
class Meta:
model = QivipQuestion
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 ...',
}))
min_amout_questions = forms.IntegerField(required=False, min_value=1, label="Minimale Anzahl an Fragen", widget=forms.NumberInput(attrs={
'class': 'border-2 border-gray-300 rounded-lg p-2 w-full'
}))
max_amout_questions = forms.IntegerField(required=False, min_value=1, label="Maximale Anzahl an Fragen", widget=forms.NumberInput(attrs={
'class': 'border-2 border-gray-300 rounded-lg p-2 w-full'
}))
user = forms.CharField(required=False, label="von User", widget=forms.TextInput(attrs={
'class': 'border-2 border-gray-300 rounded-lg p-2 w-full'
}))

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

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