Merge branch '54-filter-fur-library' into 'master'

Resolve "Filter für library"

Closes #54

See merge request enrichment-2024/qivip!21
This commit is contained in:
nik8
2025-03-22 11:59:25 +00:00
16 changed files with 448 additions and 68 deletions

2
.gitignore vendored
View File

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

View File

@@ -129,3 +129,7 @@ STATICFILES_DIRS = [BASE_DIR / 'static']
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
LOGIN_URL = '/account/login/' LOGIN_URL = '/account/login/'
import os
MEDIA_URL = '/media/' # URL, um auf Medien-Dateien zuzugreifen
MEDIA_ROOT = os.path.join(BASE_DIR, 'media') # Speicherort für hochgeladene Dateien

View File

@@ -4,9 +4,28 @@ from .models import QivipQuiz, QivipQuestion
class QuizForm(forms.ModelForm): class QuizForm(forms.ModelForm):
class Meta: class Meta:
model = QivipQuiz model = QivipQuiz
fields = ['name', 'description', 'status', 'category', 'tags',"difficulty"] fields = ['name', 'description','image', 'status', 'category', 'tags',"difficulty"]
class QuestionForm(forms.ModelForm): class QuestionForm(forms.ModelForm):
class Meta: class Meta:
model = QivipQuestion model = QivipQuestion
fields = ['quiz_id', 'data'] 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

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

View File

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

View File

@@ -24,7 +24,7 @@ class QivipQuiz(models.Model):
if value.strip() == "In dem Quiz geht es um ...": if value.strip() == "In dem Quiz geht es um ...":
raise ValidationError("Bitte gib eine aussagekräftige Beschreibung ein.") raise ValidationError("Bitte gib eine aussagekräftige Beschreibung ein.")
image = models.ImageField(max_length=256, upload_to='quiz_images/', blank=True, null=True) # Bild speichern in media/quiz_images/
uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True) uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
user_id = models.ForeignKey(User, on_delete=models.CASCADE, related_name='quiz') user_id = models.ForeignKey(User, on_delete=models.CASCADE, related_name='quiz')
creation_date = models.DateTimeField(auto_now_add=True) creation_date = models.DateTimeField(auto_now_add=True)

View File

@@ -1,6 +1,8 @@
from django.urls import path from django.urls import path
from . import views
from django.conf import settings
from . import views
from django.conf.urls.static import static
app_name = 'library' app_name = 'library'
urlpatterns = [ urlpatterns = [
@@ -12,4 +14,6 @@ urlpatterns = [
path('question/new/', views.new_question, name='new_question'), path('question/new/', views.new_question, name='new_question'),
path('question/edit/<int:pk>/', views.edit_question, name='edit_question'), path('question/edit/<int:pk>/', views.edit_question, name='edit_question'),
path('question/delete/<int:pk>/', views.delete_question, name='delete_question'), path('question/delete/<int:pk>/', views.delete_question, name='delete_question'),
] ]# Nur für die Entwicklungsumgebung
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View File

@@ -7,23 +7,109 @@ from .models import QivipQuiz, QivipQuestion
from .forms import QuizForm, QuestionForm from .forms import QuizForm, QuestionForm
import json import json
from django.core.paginator import Paginator from 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 # Übersicht aller Quizze
def overview_quiz(request): def overview_quiz(request):
#Filter
form = QuizFilterForm(request.GET)
try: try:
quizzes = QivipQuiz.objects.filter(user_id=request.user)
all_quizzes = QivipQuiz.objects.filter(status='öffentlich').exclude(user_id=request.user).filter(question__isnull=False).distinct() filter_my_quizzes = QivipQuiz.objects.filter(user_id=request.user).annotate(max_amout_questions=Count('question'))
return render(request, 'library/overview_quiz.html', {'quizzes': quizzes, 'all_quizzes': all_quizzes}) except:
filter_my_quizzes = QivipQuiz.objects.none()
filter_other_quizzes = QivipQuiz.objects.annotate(max_amout_questions=Count('question'))
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)
try:
filter_my_quizzes =filter_my_quizzes.filter(max_amout_questions__gte=min_amout_questions)
except:
filter_my_quizzes = 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
# Anzahl der Quiz pro Seite für Swiper
context = {
'show_search': True,
'filter_my_quizzes': filter_my_quizzes,
'filter_other_quizzes': filter_other_quizzes,
'form': form,
}
return render(request, 'library/overview_quiz.html', context)
try:
filter_my_quizzes = QivipQuiz.objects.filter(user_id=request.user)
filter_other_quizzes = QivipQuiz.objects.filter(status='öffentlich').exclude(user_id=request.user).filter(question__isnull=False).distinct()
context = {
'show_search': True,
'filter_my_quizzes': filter_my_quizzes,
'filter_other_quizzes': filter_other_quizzes,
'form': form,
}
return render(request, 'library/overview_quiz.html', context)
except: except:
all_quizzes = QivipQuiz.objects.filter(status='öffentlich').filter(question__isnull=False).distinct() filter_other_quizzes = QivipQuiz.objects.filter(status='öffentlich').filter(question__isnull=False).distinct()
return render(request, 'library/overview_quiz.html', {'quizzes': None, 'all_quizzes': all_quizzes}) context = {
'show_search': True,
'filter_my_quizzes': None,
'filter_other_quizzes': filter_other_quizzes,
'form': form,
}
return render(request, 'library/overview_quiz.html', context)
# Neues Quiz erstellen # Neues Quiz erstellen
@login_required @login_required
def new_quiz(request): def new_quiz(request):
if request.method == 'POST': if request.method == 'POST':
form = QuizForm(request.POST) form = QuizForm(request.POST, request.FILES)
if form.is_valid(): if form.is_valid():
quiz = form.save(commit=False) quiz = form.save(commit=False)
quiz.user_id = request.user quiz.user_id = request.user
@@ -40,7 +126,7 @@ def new_quiz(request):
def edit_quiz(request, pk): def edit_quiz(request, pk):
quiz = get_object_or_404(QivipQuiz, pk=pk, user_id=request.user) quiz = get_object_or_404(QivipQuiz, pk=pk, user_id=request.user)
if request.method == 'POST': if request.method == 'POST':
form = QuizForm(request.POST, instance=quiz) form = QuizForm(request.POST, instance=quiz, files=request.FILES)
if form.is_valid(): if form.is_valid():
form.save() form.save()
return redirect('library:detail_quiz', pk=quiz.pk) return redirect('library:detail_quiz', pk=quiz.pk)

35
django/package-lock.json generated Normal file
View File

@@ -0,0 +1,35 @@
{
"name": "django",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "django",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"swiper": "^11.2.6"
}
},
"node_modules/swiper": {
"version": "11.2.6",
"resolved": "https://registry.npmjs.org/swiper/-/swiper-11.2.6.tgz",
"integrity": "sha512-8aXpYKtjy3DjcbzZfz+/OX/GhcU5h+looA6PbAzHMZT6ESSycSp9nAjPCenczgJyslV+rUGse64LMGpWE3PX9Q==",
"funding": [
{
"type": "patreon",
"url": "https://www.patreon.com/swiperjs"
},
{
"type": "open_collective",
"url": "http://opencollective.com/swiper"
}
],
"license": "MIT",
"engines": {
"node": ">= 4.7.0"
}
}
}
}

15
django/package.json Normal file
View File

@@ -0,0 +1,15 @@
{
"name": "django",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"swiper": "^11.2.6"
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -2,7 +2,7 @@
{% block content %} {% block content %}
<div class="input-group border-blue-600 border-2 rounded-xl shadow-md mt-12"> <div class="input-group border-blue-600 border-2 rounded-xl shadow-md mt-12">
<form method="post"> <form method="post" enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
{{ form.as_p }} {{ form.as_p }}
<button type="submit">Speichern</button> <button type="submit">Speichern</button>

View File

@@ -1,61 +1,195 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block content %} {% block content %}
{% load static %}
<head>
<meta charset="utf-8" />
<title>Swiper Demo</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="{% static 'swiper/swiper-bundle.min.css' %}">
<style>
.swiper {
width: 100%;
height: 100%;
}
.swiper-slide {
text-align: center;
font-size: 18px;
background: #fff;
display: flex;
justify-content: center;
align-items: center;
}
</style>
</head>
<div class="flex justify-end mr-2"> <div class="flex justify-end mr-2">
<a href="{% url 'library:new_quiz' %}" class="mt-2 p-2 shadow-md border-blue-600 border-2 rounded-md text-black mb-12">Neues Quiz erstellen</a> <a href="{% url 'library:new_quiz' %}" class="mt-2 p-2 shadow-md border-blue-600 border-2 rounded-md text-black ">Neues Quiz erstellen</a>
</div>
<div class="flex justify-end mr-2">
<a href="{% url 'library:overview_quiz' %}" class="mt-2 p-2 shadow-md border-blue-600 border-2 rounded-md text-black ">Suche und Filter zurücksetzen</a>
</div> </div>
{% if quizzes %}
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 px-4 place-items-center">
<h1 class="font-bold mb-4 px-4 bg-blue-100">meine eigenen Quiz</h1></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 h-80 ">
<h2 class="text-lg font-bold break-words truncate"><a href="{% url 'library:edit_quiz' quiz.id %}">{{ quiz.name }}</a></h2>
<p class="text-sm text-gray-600 pt-11 h-54"><span class="break-words line-clamp-2">{{ quiz.description }} </span><br> Status: <span class="font-bold">{{ quiz.status }}</span>
<br> Schwierigkeit:<span class="font-bold"> {{ quiz.difficulty }}</span>
<br> Anzahl der Fragen:<span class="font-bold"> {{ quiz.question.count }}</span> </p>
<div class="w-screen flex justify-center items-center">
<div class="w-full m-4 border-blue-600 border-2 rounded-xl shadow-md p-6 lg:w-1/4">
<form method="get" class="flex flex-col space-y-4">
<h1 class="text-xl font-bold">Filter</h1>
Minimale Anzahl an Fragen {{ form.min_amout_questions }}
Maximale Anzahl an Fragen {{ form.max_amout_questions }}
von User {{ form.user }}
<!-- Rendert die Form-Felder -->
<button type="submit" class="qp-a-button-small border-2 border-blue-600 text-black">Filtern</button>
</form>
</div>
</div>
<!-- Swiper Container für eigene Quiz -->
{% if filter_my_quizzes %}
<div class="text-center m-8 bg-blue-600 grid mx-8 h-8 place-items-center rounded-xl shadow-md ">
<h1 id="my-quizzes" class="flex px-4 font-bold text-white">Eigene Quiz</h1>
</div>
<div> <div class="swiper mySwiper">
<div class="flex justify-between items-center gap-2"> <div class="swiper-wrapper">
<a href="#" class="qp-a-button-small border-2 border-blue-600 text-black">Spiel starten</a> {% for quiz in filter_my_quizzes %}
<div class="swiper-slide">
<div class="relative h-80 bg-white bg-cover text-white w-full max-w-md rounded-lg p-4 shadow-md border-blue-600 border-2 rounded-xl flex flex-col justify-between"
{% if quiz.image %} style="background-image: url('http://127.0.0.1:8000/library{{ quiz.image.url }}');" {% endif %}>
<div class="absolute inset-0 bg-gray-900/60 bg-opacity-50 rounded-lg"></div>
<h2 class=" font-bold break-words truncate text-white font-bold text-white drop-shadow-lg custom-outline"> <a href="{% url 'library:detail_quiz' quiz.id %}">{{ quiz.name }}</a></h2>
<p class="text-white font-bold text-white drop-shadow-lg custom-outline">
<span class="break-words line-clamp-2 ">{{ quiz.description }}</span><br>
Schwierigkeit: <span class="font-bold">{{ quiz.difficulty }}</span><br>
Anzahl der Fragen: <span class="font-bold ">{{ quiz.question.count }}</span><br>
Status: <span class="font-bold ">{{ quiz.status }}</span>
</p>
<!-- Buttons bleiben am unteren Rand -->
<div class="flex justify-between items-center gap-2 ">
<a href="#" class="qp-a-button-small border-2 border-blue-600 text-white font-bold text-white drop-shadow-lg custom-outline">Spiel starten</a>
<div class="flex gap-2"> <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:detail_quiz' quiz.id %}" class="qp-a-button-small border-2 border-blue-600 text-white text-white font-bold text-white drop-shadow-lg custom-outline">&#x270F;</a>
<a href="{% url 'library:delete_quiz' quiz.id %}" class="qp-a-button-small border-2 border-blue-600 text-white">&#x1F5D1;</a> <a href="{% url 'library:delete_quiz' quiz.id %}" class="qp-a-button-small border-2 border-blue-600 text-white text-white font-bold text-white drop-shadow-lg custom-outline">&#x1F5D1;</a>
</div>
</div>
</div> </div>
</div> </div>
</div>
</div>
{% endfor %} {% endfor %}
</div>
{% if filter_my_quizzes|length > 1 %}
<div class="swiper-button-next"></div>
<div class="swiper-button-prev"></div>
{% endif %} {% endif %}
</div> </div>
{% endif %}
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 px-4 place-items-center"> <!-- Swiper Container für Quiz von anderen Nutzern -->
<h1 class="font-bold px-4 bg-blue-100 mt-8 mb-4">Quiz von anderen Nutzern</h1></div> {% if filter_other_quizzes %}
<div class="text-center m-8 bg-blue-600 grid mx-8 h-8 place-items-center rounded-xl shadow-md">
<h1 id="other-quizzes" class="flex px-4 font-bold text-white">Quiz von anderen Nutzern</h1>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 px-4 place-items-center"> <div class="swiper mySwiper">
{% for quiz in all_quizzes %} <div class="swiper-wrapper">
<div class="h-80 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 break-words truncate">{{ quiz.name }}</h2> {% for quiz in filter_other_quizzes %}
<p class="text-sm text-gray-600 pt-11 h-54"><span class="break-words line-clamp-2">{{ quiz.description }} </span> <br> Schwierigkeit:<span class="font-bold "> <div class="swiper-slide">
{{ quiz.difficulty }}</span><br> Anzahl der Fragen:<span class="font-bold"> {{ quiz.question.count }}</span> <div class="relative h-80 bg-white bg-cover text-white w-full max-w-md rounded-lg p-4 shadow-md border-blue-600 border-2 rounded-xl flex flex-col justify-between"
<br>Erstmalig erstellt am:<span class="font-bold"> {{ quiz.creation_date }}</span> {% if quiz.image %} style="background-image: url('http://127.0.0.1:8000/library{{ quiz.image.url }}');" {% endif %}>
<div class="absolute inset-0 bg-gray-900/60 bg-opacity-50 rounded-lg"></div>
<h2 class=" font-bold break-words truncate text-white font-bold text-white drop-shadow-lg custom-outline"> <a href="{% url 'library:detail_quiz' quiz.id %}">{{ quiz.name }}</a></h2>
<p class="text-white font-bold text-white drop-shadow-lg custom-outline">
<span class="break-words line-clamp-2 ">{{ quiz.description }}</span><br>
Schwierigkeit: <span class="font-bold">{{ quiz.difficulty }}</span><br>
Anzahl der Fragen: <span class="font-bold ">{{ quiz.question.count }}</span><br>
Erstellt am: <span class="font-bold ">{{ quiz.creation_date }}</span>
</p> </p>
<div>
<div class="flex justify-between items-center gap-2"> <!-- Buttons bleiben am unteren Rand -->
<a href="#" class="qp-a-button-small border-2 border-blue-600 text-black">Spiel starten</a> <div class="flex justify-between items-center gap-2 ">
<a href="#" class="qp-a-button-small border-2 border-blue-600 text-white font-bold text-white drop-shadow-lg custom-outline">Spiel starten</a>
<a href="{% url 'library:detail_quiz' quiz.id %}" class="qp-a-button-small border-2 border-blue-600 text-white text-white font-bold text-white drop-shadow-lg custom-outline">&#x270F;</a>
<div class="flex gap-2"> <div class="flex gap-2">
<a href="{% url 'library:overview_quiz' %}?user={{ quiz.user_id }}" class="text-gray-600 text-sm">
<div class="qp-a-button-small text-gray-600 text-sm "> Quiz von {{ quiz.user_id }}</div> <button type="submit" class=" text-sm py-1 text-white font-bold text-white drop-shadow-lg custom-outline hover:text-gray-300"> Quiz von {{ quiz.user_id }}</button>
</a>
</div>
</div> </div>
</div> </div>
</div> </div>
</div>
{% endfor %} {% endfor %}
</div> </div>
{% if filter_other_quizzes|length > 1 %}
<div class="swiper-button-next"></div>
<div class="swiper-button-prev"></div>
{% endif %}
</div>
{% endif %}
{% if not filter_other_quizzes and not filter_my_quizzes %}
<div class="grid text-center font-bold items-center">Es wurde kein Quiz passendes gefunden!</div>
{% endif %}
<!-- Swiper.js -->
<script src="{% static 'swiper/swiper-bundle.min.js' %}"></script>
<script>
var swiper = new Swiper('.swiper', {
slidesPerView: getSlidesPerView(),
direction: getDirection(),
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
},
on: {
resize: function () {
swiper.changeDirection(getDirection());
swiper.params.slidesPerView = getSlidesPerView();
swiper.update();
},
},
});
function getDirection() {
return window.innerWidth <= 0? 'vertical' : 'horizontal';
}
function getSlidesPerView() {
if (window.innerWidth <= 840) {
return 1; // Smartphones
} else if (window.innerWidth <= 1024) {
return 2; // Tablets
} else {
return 3; // Desktops
}
}
</script>
<style>
.custom-outline {
-webkit-text-stroke: 0.2px rgb(0, 0, 0); /* Schwarze Umrandung */
}
</style>
{% endblock %} {% endblock %}

View File

@@ -1,10 +1,27 @@
<nav class="flex justify-between bg-blue-600 h-12 px-4 sm:px-6 lg:px-8 rounded-lg m-2 shadow-md"> <nav class="flex justify-between items-center bg-blue-600 h-12 px-4 sm:px-6 lg:px-8 rounded-lg m-2 shadow-md overflow-x-auto whitespace-nowrap">
<div class="flex items-center"> <div class="flex items-center flex-shrink-0">
<h2 class="text-xl font-bold text-white hover:scale-110 transition duration-200"><a href="{% url 'homepage:home' %}">qivip</a></h2> <h2 class="text-xl font-bold text-white hover:scale-110 transition duration-200">
<a href="{% url 'homepage:home' %}">qivip</a>
</h2>
</div> </div>
<div class="flex items-center">
{% if show_search %}
<form method="get" class="mr-2 ml-2 flex items-center w-full max-w-sm bg-white rounded-full px-4 py-1 shadow-md">
<input type="text" name="search" placeholder="Suche ..." value="{{ request.GET.search }}"
class="w-full px-3 py-1 text-black bg-transparent focus:outline-none">
<button type="submit" class=" py-1 text-blue-600">
🔍
</button>
</form>
{% endif %}
<div class="flex items-center space-x-4">
<ul class="flex space-x-4 qp-nav-list"> <ul class="flex space-x-4 qp-nav-list">
{% if show_search %}
<li class="hidden md:flex"><a href="{% url 'library:overview_quiz' %}">Bibliothek</a></li>
{% else%}
<li><a href="{% url 'library:overview_quiz' %}">Bibliothek</a></li> <li><a href="{% url 'library:overview_quiz' %}">Bibliothek</a></li>
{% endif %}
{% if user.is_authenticated %} {% if user.is_authenticated %}
<li><a href="{% url 'accounts:home' %}">Konto</a></li> <li><a href="{% url 'accounts:home' %}">Konto</a></li>
{% else %} {% else %}

View File

@@ -1,3 +1,4 @@
django~=5.1.4 django~=5.1.4
channels~=4.2.0 channels~=4.2.0
daphne~=4.1.2 daphne~=4.1.2
pillow~=11.1.0