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

4
.gitignore vendored
View File

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

View File

@@ -128,4 +128,8 @@ STATICFILES_DIRS = [BASE_DIR / 'static']
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 Meta:
model = QivipQuiz
fields = ['name', 'description', 'status', 'category', 'tags',"difficulty"]
fields = ['name', 'description','image', 'status', 'category', 'tags',"difficulty"]
class QuestionForm(forms.ModelForm):
class Meta:
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 ...":
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)
user_id = models.ForeignKey(User, on_delete=models.CASCADE, related_name='quiz')
creation_date = models.DateTimeField(auto_now_add=True)

View File

@@ -1,6 +1,8 @@
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'
urlpatterns = [
@@ -12,4 +14,6 @@ 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'),
]
]# 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
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
def overview_quiz(request):
#Filter
form = QuizFilterForm(request.GET)
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()
return render(request, 'library/overview_quiz.html', {'quizzes': quizzes, 'all_quizzes': all_quizzes})
filter_my_quizzes = QivipQuiz.objects.filter(user_id=request.user).annotate(max_amout_questions=Count('question'))
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:
all_quizzes = QivipQuiz.objects.filter(status='öffentlich').filter(question__isnull=False).distinct()
return render(request, 'library/overview_quiz.html', {'quizzes': None, 'all_quizzes': all_quizzes})
filter_other_quizzes = QivipQuiz.objects.filter(status='öffentlich').filter(question__isnull=False).distinct()
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
@login_required
def new_quiz(request):
if request.method == 'POST':
form = QuizForm(request.POST)
form = QuizForm(request.POST, request.FILES)
if form.is_valid():
quiz = form.save(commit=False)
quiz.user_id = request.user
@@ -40,7 +126,7 @@ 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)
form = QuizForm(request.POST, instance=quiz, files=request.FILES)
if form.is_valid():
form.save()
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 %}
<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 %}
{{ form.as_p }}
<button type="submit">Speichern</button>

View File

@@ -1,61 +1,195 @@
{% extends 'base.html' %}
{% 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">
<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>
{% 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>
<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 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>
{% endfor %}
{% endif %}
</div>
<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 px-4 bg-blue-100 mt-8 mb-4">Quiz von anderen Nutzern</h1></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 class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 px-4 place-items-center">
{% for quiz in all_quizzes %}
<div class="swiper mySwiper">
<div class="swiper-wrapper">
{% 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>
<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>
<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 ">
{{ quiz.difficulty }}</span><br> Anzahl der Fragen:<span class="font-bold"> {{ quiz.question.count }}</span>
<br>Erstmalig erstellt am:<span class="font-bold"> {{ quiz.creation_date }}</span>
</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">
<div class="qp-a-button-small text-gray-600 text-sm "> Quiz von {{ quiz.user_id }}</div>
</div>
</div>
<!-- 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">
<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 text-white font-bold text-white drop-shadow-lg custom-outline">&#x1F5D1;</a>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% if filter_my_quizzes|length > 1 %}
<div class="swiper-button-next"></div>
<div class="swiper-button-prev"></div>
{% endif %}
</div>
{% endif %}
<!-- Swiper Container für Quiz von anderen Nutzern -->
{% 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="swiper mySwiper">
<div class="swiper-wrapper">
{% for quiz in filter_other_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>
Erstellt am: <span class="font-bold ">{{ quiz.creation_date }}</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>
<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">
<a href="{% url 'library:overview_quiz' %}?user={{ quiz.user_id }}" class="text-gray-600 text-sm">
<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>
{% endfor %}
</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 %}

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">
<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>
<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 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>
</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">
{% 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>
{% endif %}
{% if user.is_authenticated %}
<li><a href="{% url 'accounts:home' %}">Konto</a></li>
{% else %}

View File

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