Resolve "Auf ähnliche Quizze verweisen" #287

Merged
ben8 merged 1 commits from 163-auf-ahnliche-quizze-verweisen into master 2025-12-03 14:22:02 +00:00
16 changed files with 317 additions and 123 deletions
Showing only changes of commit b596655e43 - Show all commits

View File

@@ -0,0 +1,45 @@
| Name | Version | License |
|---------------------------|--------------|---------------------------------------------------------|
| Automat | 25.4.16 | MIT License |
| Django | 5.1.13 | BSD License |
| Twisted | 25.5.0 | MIT License |
| asgiref | 3.10.0 | BSD License |
| attrs | 25.4.0 | UNKNOWN |
| autobahn | 25.10.1 | UNKNOWN |
| certifi | 2025.10.5 | Mozilla Public License 2.0 (MPL 2.0) |
| cffi | 2.0.0 | UNKNOWN |
| channels | 4.2.2 | BSD License |
| channels_redis | 4.3.0 | BSD |
| chardet | 5.2.0 | GNU Lesser General Public License v2 or later (LGPLv2+) |
| charset-normalizer | 3.4.4 | MIT |
| colorama | 0.4.6 | BSD License |
| constantly | 23.10.4 | MIT License |
| cryptography | 46.0.3 | UNKNOWN |
| daphne | 4.1.2 | BSD License |
| django-autocomplete-light | 3.12.1 | MIT License |
| django-cleanup | 9.0.0 | MIT License |
| geographiclib | 2.1 | MIT |
| geopy | 2.4.1 | MIT License |
| hyperlink | 21.0.0 | MIT License |
| idna | 3.11 | UNKNOWN |
| incremental | 24.7.2 | MIT License |
| msgpack | 1.1.2 | UNKNOWN |
| pillow | 11.1.0 | CMU License (MIT-CMU) |
| pyOpenSSL | 25.3.0 | Apache Software License |
| pyasn1 | 0.6.1 | BSD License |
| pyasn1_modules | 0.4.2 | BSD License |
| pycparser | 2.23 | BSD License |
| pypng | 0.20220715.0 | MIT License |
| python-dotenv | 1.1.1 | BSD License |
| qrcode | 7.4.2 | BSD License |
| redis | 6.4.0 | MIT License |
| reportlab | 4.2.5 | BSD License |
| requests | 2.32.5 | Apache Software License |
| service-identity | 24.2.0 | MIT License |
| sqlparse | 0.5.3 | BSD License |
| txaio | 25.9.2 | UNKNOWN |
| typing_extensions | 4.15.0 | UNKNOWN |
| tzdata | 2025.2 | Apache Software License |
| urllib3 | 2.5.0 | UNKNOWN |
| whitenoise | 6.6.0 | MIT License |
| zope.interface | 8.0.1 | Zope Public License |

View File

@@ -1,44 +0,0 @@
| Name | Version | License |
|--------------------|--------------|---------------------------------------------------------|
| Automat | 25.4.16 | MIT License |
| Django | 5.1.13 | BSD License |
| Twisted | 25.5.0 | MIT License |
| asgiref | 3.10.0 | BSD License |
| attrs | 25.4.0 | UNKNOWN |
| autobahn | 25.10.1 | UNKNOWN |
| certifi | 2025.10.5 | Mozilla Public License 2.0 (MPL 2.0) |
| cffi | 2.0.0 | UNKNOWN |
| channels | 4.2.2 | BSD License |
| channels_redis | 4.3.0 | BSD |
| chardet | 5.2.0 | GNU Lesser General Public License v2 or later (LGPLv2+) |
| charset-normalizer | 3.4.4 | MIT |
| colorama | 0.4.6 | BSD License |
| constantly | 23.10.4 | MIT License |
| cryptography | 46.0.3 | UNKNOWN |
| daphne | 4.1.2 | BSD License |
| django-cleanup | 9.0.0 | MIT License |
| geographiclib | 2.1 | MIT |
| geopy | 2.4.1 | MIT License |
| hyperlink | 21.0.0 | MIT License |
| idna | 3.11 | UNKNOWN |
| incremental | 24.7.2 | MIT License |
| msgpack | 1.1.2 | UNKNOWN |
| pillow | 11.1.0 | CMU License (MIT-CMU) |
| pyOpenSSL | 25.3.0 | Apache Software License |
| pyasn1 | 0.6.1 | BSD License |
| pyasn1_modules | 0.4.2 | BSD License |
| pycparser | 2.23 | BSD License |
| pypng | 0.20220715.0 | MIT License |
| python-dotenv | 1.1.1 | BSD License |
| qrcode | 7.4.2 | BSD License |
| redis | 6.4.0 | MIT License |
| reportlab | 4.2.5 | BSD License |
| requests | 2.32.5 | Apache Software License |
| service-identity | 24.2.0 | MIT License |
| sqlparse | 0.5.3 | BSD License |
| txaio | 25.9.2 | UNKNOWN |
| typing_extensions | 4.15.0 | UNKNOWN |
| tzdata | 2025.2 | Apache Software License |
| urllib3 | 2.5.0 | UNKNOWN |
| whitenoise | 6.6.0 | MIT License |
| zope.interface | 8.0.1 | Zope Public License |

View File

@@ -57,6 +57,8 @@ INSTALLED_APPS = [
'channels', 'channels',
'django_cleanup', 'django_cleanup',
'django.contrib.humanize', 'django.contrib.humanize',
'dal',
'dal_select2',
] ]
MIDDLEWARE = [ MIDDLEWARE = [

View File

@@ -6,6 +6,12 @@ class QivipQuizAdmin(admin.ModelAdmin):
list_display = ('name','published_at', 'user_id', 'status', 'category', 'creation_date', 'update_date') # Welche Felder sollen in der Übersicht angezeigt werden list_display = ('name','published_at', 'user_id', 'status', 'category', 'creation_date', 'update_date') # Welche Felder sollen in der Übersicht angezeigt werden
search_fields = ('name', 'user_id__username', 'category__name') # Suchfelder search_fields = ('name', 'user_id__username', 'category__name') # Suchfelder
list_filter = ('status', 'category') # Filteroptionen list_filter = ('status', 'category') # Filteroptionen
autocomplete_fields = ("reference_quizzes",)
# Für das QivipQuestion Modell # Für das QivipQuestion Modell
class QivipQuestionAdmin(admin.ModelAdmin): class QivipQuestionAdmin(admin.ModelAdmin):
list_display = ('id', 'quiz_id', 'data', 'creation_date', 'update_date', 'credits') list_display = ('id', 'quiz_id', 'data', 'creation_date', 'update_date', 'credits')

View File

@@ -1,17 +1,31 @@
from django import forms from django import forms
from .models import QivipQuiz, QivipQuestion, QuizCategory from .models import QivipQuiz, QivipQuestion, QuizCategory
from dal import autocomplete
class QuizForm(forms.ModelForm): class QuizForm(forms.ModelForm):
class Meta: class Meta:
model = QivipQuiz model = QivipQuiz
fields = ['name', 'description', 'status', 'category','difficulty','credits'] fields = ['name', 'description', 'status', 'category','difficulty','reference_quizzes','credits']
widgets = {
'reference_quizzes': autocomplete.ModelSelect2Multiple(
url='library:quiz-autocomplete',
forward=['pk']
)
}
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(QuizForm, self).__init__(*args, **kwargs) super(QuizForm, self).__init__(*args, **kwargs)
for field in self.fields.values(): for field in self.fields.values():
field.widget.attrs.update({ field.widget.attrs.update({
'class': 'dark:bg-[#2a2f3a]! dark:text-white!'
}) })
self.fields["reference_quizzes"].queryset = QivipQuiz.objects.filter(
status="öffentlich").exclude(pk=self.instance.pk)
self.fields["reference_quizzes"].label_from_instance = lambda obj: f"{obj.name} (ID: {obj.pk}) (User: {obj.user_id})"

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-11-27 18:22
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0059_remove_qivipquiz_single_player_or_multiple'),
]
operations = [
migrations.AddField(
model_name='qivipquiz',
name='similar_quizzes',
field=models.ManyToManyField(blank=True, related_name='similar_to', to='library.qivipquiz'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-11-27 18:22
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0060_qivipquiz_similar_quizzes'),
]
operations = [
migrations.AlterField(
model_name='qivipquiz',
name='similar_quizzes',
field=models.ManyToManyField(blank=True, to='library.qivipquiz'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-11-27 19:54
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0061_alter_qivipquiz_similar_quizzes'),
]
operations = [
migrations.AlterField(
model_name='qivipquiz',
name='similar_quizzes',
field=models.ManyToManyField(blank=True, to='library.qivipquiz', verbose_name='Verweis auf andere Quizze'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.7 on 2025-12-03 11:02
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('library', '0062_alter_qivipquiz_similar_quizzes'),
]
operations = [
migrations.RenameField(
model_name='qivipquiz',
old_name='similar_quizzes',
new_name='reference_quizzes',
),
]

View File

@@ -43,6 +43,12 @@ class QivipQuiz(models.Model):
on_delete=models.PROTECT, on_delete=models.PROTECT,
related_name="quizzes" related_name="quizzes"
) )
reference_quizzes = models.ManyToManyField(
'self',
blank=True,
symmetrical=False,
verbose_name="Verweis auf andere Quizze"
)
quiz_played_Lernmodus = models.IntegerField(null=True, blank=True, default=0) quiz_played_Lernmodus = models.IntegerField(null=True, blank=True, default=0)

View File

@@ -3,6 +3,7 @@ from django.urls import path
from django.conf import settings from django.conf import settings
from . import views from . import views
from django.conf.urls.static import static from django.conf.urls.static import static
from .views import QuizAutocomplete
app_name = 'library' app_name = 'library'
urlpatterns = [ urlpatterns = [
@@ -20,7 +21,11 @@ urlpatterns = [
path('detail/copy/<int:pk>/', views.copy_quiz, name='copy_quiz'), path('detail/copy/<int:pk>/', views.copy_quiz, name='copy_quiz'),
path('favorite_quiz/<int:pk>/', views.favorite_quiz, name='favorite_quiz'), path('favorite_quiz/<int:pk>/', views.favorite_quiz, name='favorite_quiz'),
path("quiz-names-json/", views.quiz_names_json, name="quiz_names_json"), path("quiz-names-json/", views.quiz_names_json, name="quiz_names_json"),
#path('quiz/<int:pk>/pdf/', views.pdf_solution, name='quiz_solution'), path(
#path('quiz/<int:pk>/pdf/task', views.pdf_task, name='quiz_task'), "autocomplete/quiz",
QuizAutocomplete.as_view(),
name="quiz-autocomplete"
),
] ]

View File

@@ -40,6 +40,37 @@ from PIL import Image as PILImage, ImageOps
from django.conf import settings from django.conf import settings
import os import os
from django.utils.html import escape from django.utils.html import escape
from .models import QivipQuiz
from django.db.models import Value, CharField
from django.db.models.functions import Concat
# views.py
from dal import autocomplete
from .models import QivipQuiz
class QuizAutocomplete(autocomplete.Select2QuerySetView):
def get_queryset(self):
qs = QivipQuiz.objects.filter(status="öffentlich")
pk = self.forwarded.get('pk', None)
if pk:
qs = qs.exclude(pk=pk)
if self.q:
qs = qs.annotate(
search_text=Concat(
'name',
Value(' (ID: '), 'pk', Value(') (User: '), 'user_id', Value(')'),
output_field=CharField()
)
).filter(search_text__icontains=self.q)
return qs
def get_result_label(self, result):
# z.B. Name + ID + User
return f"{result.name} (ID: {result.pk}) (User: {result.user_id})"
""" """
def pdf_solution(request, pk): def pdf_solution(request, pk):
quiz = get_object_or_404(QivipQuiz, pk=pk) quiz = get_object_or_404(QivipQuiz, pk=pk)

View File

@@ -1,5 +1,11 @@
@import 'tailwindcss'; @import 'tailwindcss';
.select2-container {
@apply font-black!;
}
qp-container { qp-container {
@apply max-w-xl w-full mx-auto; @apply max-w-xl w-full mx-auto;
} }

View File

@@ -319,65 +319,61 @@
{% endif %} {% endif %}
</div> </div>
<div class="flex justify-end">
<div class="max-w-full p-4 mt-8 rounded-2xl bg-gray-100 dark:bg-transparent shadow-sm">
<p class="text-lg font-bold text-gray-800 dark:text-white dark:bg-transparent mb-3">
Quiz-Infos
</p>
<div class="flex">
{% if quiz.base_quiz %} {% if quiz.base_quiz %}
<p class="text-gray-600 mt-2 dark:text-white">Bearbeitung:<span class="text-blue-600 font-bold"> <a href="{% url 'library:overview_quiz' %}?filter=1&user={{ quiz.user_id }}">{{ quiz.user_id }}</a></span></p> <p class="text-gray-600 mt-2 dark:text-white">Bearbeitung:<span class="font-bold"> <a href="{% url 'library:overview_quiz' %}?filter=1&user={{ quiz.user_id }}">{{ quiz.user_id }}</a></span></p>
{% else %} {% else %}
<p class="text-gray-600 mt-2 dark:text-white">Autor:<span class="text-blue-600 font-bold"> <a href="{% url 'library:overview_quiz' %}?filter=1&user={{ quiz.user_id }}">{{ quiz.user_id }}</a></span></p> <p class="text-gray-600 mt-2 dark:text-white">Autor:<span class="font-bold"> <a href="{% url 'library:overview_quiz' %}?filter=1&user={{ quiz.user_id }}">{{ quiz.user_id }}</a></span></p>
{% endif %} {% endif %}
</div> </div>
<div class="flex justify-end"> <div class="flex">
{% if quiz.base_quiz %} {% if quiz.base_quiz %}
<p class="text-gray-600 dark:text-white ">Kopie von: <a href="{% url 'library:detail_quiz' quiz.base_quiz.id %}"><span class="text-blue-600 font-bold">{{ quiz.base_quiz}}</span></a></p> <p class="text-gray-600 dark:text-white ">Kopie von: <a href="{% url 'library:detail_quiz' quiz.base_quiz.id %}"><span class="font-bold">{{ quiz.base_quiz}}</span></a></p>
{% endif %} {% endif %}
</div> </div>
<div class="flex justify-end"> <div class="flex">
{% if quiz.base_quiz %} {% if quiz.base_quiz %}
<p class="text-gray-600 dark:text-white">Autor:<span class="text-blue-600 font-bold"> <a href="{% url 'library:overview_quiz' %}?filter=1&user={{ quiz.base_quiz.user_id }}">{{ quiz.base_quiz.user_id }}</a></span></p> <p class="text-gray-600 dark:text-white">Autor:<span class="font-bold"> <a href="{% url 'library:overview_quiz' %}?filter=1&user={{ quiz.base_quiz.user_id }}">{{ quiz.base_quiz.user_id }}</a></span></p>
{% endif %} {% endif %}
</div> </div>
<!-- Links müssen wieder ergänzt werden!
{% if questions %}
<div class="flex flex-col sm:flex-row gap-4 items-start sm:items-center my-6">
<div class="flex">
<p class="text-gray-600 dark:text-white">Quiz ID:<span class="font-bold"> {{ quiz.pk }}</span></p>
<a href=""
target="_blank"
class=" inline-flex items-center gap-2 px-2 py-2 rounded-lg bg-blue-600 hover:bg-blue-700 text-white shadow-md transition">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none"
viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 4v16m8-8H4" />
</svg>
als Aufgabenblatt herunterladen (PDF)
</a>
<a href=""
target="_blank"
class="inline-flex items-center gap-2 px-2 py-2 rounded-lg bg-green-600 hover:bg-green-700 text-white shadow-md transition">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none"
viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 4v16m8-8H4" />
</svg>
Lösungen herunterladen (PDF)
</a>
</div> </div>
{% endif %}
-->
{% if quiz.reference_quizzes.all %}
<div class="flex">
<p class="text-gray-600 dark:text-white">Verweis auf andere Quizze:
<span class="font-bold">
{% for reference in quiz.reference_quizzes.all %}
<a href="{% url 'library:detail_quiz' reference.id %}">{{ reference.name}}</a>{% if not forloop.last %}, {% endif %}
{% empty %}
{% endfor %}
</span></p>
</div>
{% endif %}
</div>
<div class="max-w-full p-4 mt-8 rounded-2xl bg-gray-100 dark:bg-transparent shadow-sm"> <div class="max-w-full p-4 mt-8 rounded-2xl bg-gray-100 dark:bg-transparent shadow-sm">
<div class="flex flex-col max-w-full"> <div class="flex flex-col max-w-full">
<p class="text-lg font-bold text-gray-800 dark:text-white dark:bg-transparent mb-3"> <p class="text-lg font-bold text-gray-800 dark:text-white dark:bg-transparent mb-3">
Credits Credits

View File

@@ -1,43 +1,97 @@
{% extends 'base.html' %} {% extends 'base.html' %} {% block content %}
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
{{ form.media }}
<style>
{% block content %} .select2-container {
width: 100% !important;
min-width: 0; /* verhindert Überlaufen bei kleinen Screens */
box-sizing: border-box;
}
/* Das Auswahlfeld */
.select2-selection {
width: 100% !important;
box-sizing: border-box;
}
/* Angezeigter Text im Auswahlfeld */
.select2-selection__rendered {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.select2-selection__choice {
white-space: normal;
word-break: break-word;
overflow-wrap: break-word;
max-width: 100%;
}
<div class="container mx-auto px-4 py-4">
<div class="max-w-lg field-group mx-auto bg-white rounded-lg shadow-md p-6 border-blue-600 border-2 rounded-xl shadow-md dark:bg-transparent dark:text-white">
<form method="post" enctype="multipart/form-data" class="dark-form space-y-4">
{% csrf_token %}
{{ form.as_p }}
{% if form.instance.image %}
<img src="{{ form.instance.image.image.url }}" style="max-width: 100px;" alt="Bild der Frage">
<label for="reset_image">
<input type="checkbox" name="reset_image" id="reset_image">
Bild zurücksetzen
</label>
{% endif %}
<div>
<label class="font-bold" for="quiz_image">Bild:</label>
<input class="max-w-full bg-blue-200 p-2 m-2 rounded-lg border-2 border-blue-400 dark:bg-transparent" type="file" name="quiz_image" id="quiz_image">
</div>
<div class="text-center text-gray-600 text-sm dislay-flex align-bottom">
<label class=''>Mit der Erstellung von diesem Quiz stimmen Sie der <a class="text-blue-600" href="/privacy">Datenschutzerklärung </a> zu.</label>
</div>
<div class="flex justify-center space-x-3"> </style>
<a href="{% if object.quiz_id %}{% url 'library:detail_quiz' object.quiz_id.id %}{% else %}{% url 'library:overview_quiz' %}{% endif %}"
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"> <input type="hidden" name="pk" value="{{ form.instance.pk }}" />
Abbrechen
</a> <div class="container mx-auto px-4 py-4">
<button type="submit" <div
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"> class="max-w-lg field-group mx-auto bg-white rounded-lg shadow-md p-6 border-blue-600 border-2 rounded-xl shadow-md dark:bg-transparent dark:text-white"
Speichern >
</button> <form
</div> method="post"
enctype="multipart/form-data"
class="dark-form space-y-4"
>
{% csrf_token %} {{ form.as_p }} {% if form.instance.image %}
<img
src="{{ form.instance.image.image.url }}"
style="max-width: 100px"
alt="Bild der Frage"
/>
<label for="reset_image">
<input type="checkbox" name="reset_image" id="reset_image" />
Bild zurücksetzen
</label>
{% endif %}
<div>
<label class="font-bold" for="quiz_image">Bild:</label>
<input
class="max-w-full bg-blue-200 p-2 m-2 rounded-lg border-2 border-blue-400 dark:bg-transparent"
type="file"
name="quiz_image"
id="quiz_image"
/>
</div>
<div class="text-center text-gray-600 text-sm dislay-flex align-bottom">
<label class=""
>Mit der Erstellung von diesem Quiz stimmen Sie der
<a class="text-blue-600" href="/privacy">Datenschutzerklärung </a>
zu.</label
>
</div>
<div class="flex justify-center space-x-3">
<a
href="{% if object.quiz_id %}{% url 'library:detail_quiz' object.quiz_id.id %}{% else %}{% url 'library:overview_quiz' %}{% endif %}"
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"
>
Speichern
</button>
</div>
</form> </form>
</div>
</div> </div>
</div> {% endblock %}
{% endblock %}

View File

@@ -14,3 +14,4 @@ pip-licenses ~= 5.0.0
requests requests
requests requests
geopy geopy
django-autocomplete-light