critizise Quiz

This commit is contained in:
ben8
2026-01-17 11:11:34 +01:00
parent 1fb0958daf
commit fb225dc6f3
20 changed files with 402 additions and 1128 deletions

View File

@@ -6,7 +6,7 @@ from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from django.urls import reverse
from django.contrib import messages
from .models import QivipQuiz, QivipQuestion, QivipQuizFavorite, QuestionImage, QuizImage
from .models import QivipQuiz, QivipQuestion, QivipQuizFavorite, QuestionImage, QuizCriticism, QuizImage
from .forms import QuizForm, QuestionForm
import json
from django.core.paginator import Paginator
@@ -70,306 +70,7 @@ class QuizAutocomplete(autocomplete.Select2QuerySetView):
# z.B. Name + ID + User
return f"{result.name} (ID: {result.pk}) (User: {result.user_id})"
"""
def pdf_solution(request, pk):
quiz = get_object_or_404(QivipQuiz, pk=pk)
questions = QivipQuestion.objects.filter(quiz_id=quiz)
for question in questions:
if question.data:
question.data = json.loads(question.data)
response = HttpResponse(content_type='application/pdf')
response['Content-Disposition'] = f'attachment; filename="Lösungen - {quiz.name}.pdf"'
doc = SimpleDocTemplate(response, pagesize=A4,
rightMargin=40, leftMargin=40,
topMargin=60, bottomMargin=60)
styles = getSampleStyleSheet()
# Stil für Quiz-Titel
title_style = ParagraphStyle(
'TitleStyle',
parent=styles['Title'],
alignment=1, # zentriert
fontSize=14,
spaceAfter=2,
)
# Stil für Fragen-Box
question_box_style = ParagraphStyle(
'QuestionBox',
fontSize=10,
leading=16,
fontName='Helvetica-Bold',
spaceAfter=8,
leftIndent=6,
rightIndent=6,
alignment=0
)
# Stil für Antwort (richtig)
answer_correct_style = ParagraphStyle(
'AnswerCorrect',
fontSize=11,
leading=14,
textColor=colors.green,
leftIndent=20,
spaceAfter=4,
)
# Stil für Antwort (falsch)
answer_wrong_style = ParagraphStyle(
'AnswerWrong',
fontSize=11,
leading=14,
textColor=colors.red,
leftIndent=20,
spaceAfter=4,
)
# Stil für normalen Antwort-Text (ohne Antwortanzeige)
answer_neutral_style = ParagraphStyle(
'AnswerNeutral',
fontSize=11,
leading=14,
textColor=colors.black,
leftIndent=20,
spaceAfter=4,
)
date_style = ParagraphStyle(
name='RightAligned',
parent=styles['Normal'],
alignment=2,
)
story = []
story.append(Paragraph(f"Quiz: {quiz.name}", title_style))
story.append(Paragraph(f"- Lösungen -", title_style))
story.append(Spacer(1, 20))
story.append(Paragraph(datetime.today().strftime("%d.%m.%Y"), date_style))
story.append(Spacer(1, 20))
if quiz.description:
story.append(Paragraph(quiz.description, styles['Normal']))
story.append(Spacer(1, 20))
for i, question in enumerate(questions, start=1):
data = question.data
frage_text = data.get('question', 'Frage nicht gefunden.')
story.append(Paragraph(f"Frage {i}: {frage_text}", question_box_style))
story.append(Spacer(1, 6))
rl_image = None
if hasattr(question, 'image') and question.image and question.image.image:
image_url = request.build_absolute_uri(question.image.image.url)
img_response = requests.get(image_url)
if img_response.status_code == 200:
image_data = BytesIO(img_response.content)
pil_image = PILImage.open(image_data)
pil_image = ImageOps.exif_transpose(pil_image)
orig_width, orig_height = pil_image.size
max_width = 80
max_height = 80
ratio = min(max_width / orig_width, max_height / orig_height)
new_width = orig_width * ratio
new_height = orig_height * ratio
output = BytesIO()
pil_image.save(output, format='JPEG')
output.seek(0)
rl_image = Image(output, width=new_width, height=new_height)
options = data.get('options', [])
answer_paragraphs = []
for idx, option in enumerate(options, start=1):
opt_text = option.get('value', '')
if option.get('is_correct', False):
text = f"{opt_text}"
style = answer_correct_style
else:
text = f"{opt_text}"
style = answer_wrong_style
answer_paragraphs.append(Paragraph(text, style))
if rl_image:
# Tabelle mit Bild links und Antworten rechts
from reportlab.platypus import Table, TableStyle
from reportlab.lib.units import cm
data_table = [[rl_image, answer_paragraphs]]
table = Table(data_table, colWidths=[6*cm, 10*cm])
table.setStyle(TableStyle([
('VALIGN', (0,0), (-1,-1), 'TOP'),
('LEFTPADDING', (0,0), (-1,-1), 0),
('RIGHTPADDING', (0,0), (-1,-1), 10),
]))
story.append(table)
else:
# Kein Bild, Antworten einfach nacheinander
for p in answer_paragraphs:
story.append(p)
story.append(Spacer(1, 12))
story.append(HRFlowable(width="100%", thickness=1, color=colors.grey))
story.append(Spacer(1, 12))
current_url = request.build_absolute_uri(f"/library/detail/{quiz.pk}/")
escaped_url = escape(current_url) # Sicherheitshalber escapen
link_html = f'<a href="{escaped_url}" color="blue">{escaped_url}</a>'
story.append(Paragraph(link_html, styles['Normal']))
doc.build(story)
return response
def pdf_task(request, pk):
quiz = get_object_or_404(QivipQuiz, pk=pk)
questions = QivipQuestion.objects.filter(quiz_id=quiz)
for question in questions:
if question.data:
question.data = json.loads(question.data)
response = HttpResponse(content_type='application/pdf')
response['Content-Disposition'] = f'attachment; filename="Aufgabenblatt - {quiz.name}.pdf"'
doc = SimpleDocTemplate(response, pagesize=A4,
rightMargin=40, leftMargin=40,
topMargin=60, bottomMargin=60)
styles = getSampleStyleSheet()
title_style = ParagraphStyle(
'TitleStyle',
parent=styles['Title'],
alignment=1,
fontSize=14,
spaceAfter=2,
fontName='Helvetica-Bold',
)
question_box_style = ParagraphStyle(
'QuestionBox',
fontSize=10,
leading=16,
fontName='Helvetica-Bold',
spaceAfter=8,
leftIndent=6,
rightIndent=6,
alignment=0
)
answer_neutral_style = ParagraphStyle(
'AnswerNeutral',
fontSize=11,
leading=14,
textColor=colors.black,
leftIndent=20,
spaceAfter=4,
)
date_style = ParagraphStyle(
name='RightAligned',
parent=styles['Normal'],
alignment=2,
)
name_style = ParagraphStyle(
name='RightAligned',
parent=styles['Normal'],
alignment=0,
)
story = []
# Titel & Datum
story.append(Paragraph(f"Aufgabenblatt: {quiz.name}", title_style))
story.append(Spacer(1, 20))
story.append(Paragraph(datetime.today().strftime("%d.%m.%Y"), date_style))
story.append(Spacer(1, 20))
story.append(Paragraph("Name: ", name_style))
story.append(Spacer(1, 20))
if quiz.description:
story.append(Paragraph(quiz.description, styles['Normal']))
story.append(Spacer(1, 20))
for i, question in enumerate(questions, start=1):
data = question.data
frage_text = data.get('question', 'Frage nicht gefunden.')
story.append(Paragraph(f"Frage {i}: {frage_text}", question_box_style))
story.append(Spacer(1, 6))
rl_image = None
if hasattr(question, 'image') and question.image and question.image.image:
image_url = request.build_absolute_uri(question.image.image.url)
img_response = requests.get(image_url)
if img_response.status_code == 200:
image_data = BytesIO(img_response.content)
pil_image = PILImage.open(image_data)
pil_image = ImageOps.exif_transpose(pil_image)
orig_width, orig_height = pil_image.size
max_width = 80 # gleich wie bei Lösungen
max_height = 80
ratio = min(max_width / orig_width, max_height / orig_height)
new_width = orig_width * ratio
new_height = orig_height * ratio
output = BytesIO()
pil_image.save(output, format='JPEG')
output.seek(0)
rl_image = Image(output, width=new_width, height=new_height)
options = data.get('options', [])
answer_paragraphs = []
for idx, option in enumerate(options, start=1):
opt_text = option.get('value', '')
if data.get("type") !="input":
opt_text = option.get('value', '')
answer_paragraphs.append(Paragraph(f"{idx}. {opt_text}", answer_neutral_style))
else:
story.append(Paragraph("Deine Antwort: ", answer_neutral_style))
story.append(Spacer(1, 12))
if rl_image:
from reportlab.platypus import Table, TableStyle
from reportlab.lib.units import cm
data_table = [[rl_image, answer_paragraphs]]
table = Table(data_table, colWidths=[6*cm, 10*cm])
table.setStyle(TableStyle([
('VALIGN', (0,0), (-1,-1), 'TOP'),
('LEFTPADDING', (0,0), (-1,-1), 0),
('RIGHTPADDING', (0,0), (-1,-1), 10),
]))
story.append(table)
else:
for p in answer_paragraphs:
story.append(p)
story.append(Spacer(1, 12))
story.append(HRFlowable(width="100%", thickness=1, color=colors.grey))
story.append(Spacer(1, 12))
current_url = request.build_absolute_uri(f"/library/detail/{quiz.pk}/")
escaped_url = escape(current_url) # Sicherheitshalber escapen
link_html = f'<a href="{escaped_url}" color="blue">{escaped_url}</a>'
story.append(Paragraph(link_html, styles['Normal']))
doc.build(story)
return response
"""
def quiz_names_json(request):
@@ -430,7 +131,82 @@ def apply_quiz_filters(queryset, form):
return queryset.filter(**filters)
def critisize_quiz(request, pk):
quiz= get_object_or_404(QivipQuiz, pk=pk)
if request.method == "POST":
question_id = request.POST.get("question_id") or "whole_quiz"
if question_id!="whole_quiz":
question = quiz.questions.get(id=question_id)
data = json.loads(question.data)
question_text = data["question"]
else:
question_text="Quiz(formular) kritisiert"
question = None
question_id = None
critic_type = request.POST.get("critic")
CRITICISM_MAP = {
"content_error": "Die Frage oder die Antworten enthalten einen inhaltlichen Fehler.",
"spelling_error": "Die Frage oder die Antworten enthalten (einen oder mehrere) Rechtschreibfehler.",
"logic_error": "Die Frage macht keinen Sinn oder ist nur sehr schwer zu verstehen.",
"inappropriate_error": "Die Frage, die Antworten oder Bilder sind unangemessen.",
"not_completed_error":"Die Antworten sind unvollständig oder es wurde keine richtige Antwort ausgewählt.",
"quiz_error":"Das Quiz(formular) selbst ist unangebracht, falsch oder nicht verständlich."
}
exists = QuizCriticism.objects.filter(
id_question=question_id,
question=question,
quiz=quiz,
user=request.user,
question_criticism=question_text,
criticism=CRITICISM_MAP[critic_type],
).exists()
if exists:
messages.error(request, "Diese Kritik wurde bereits abgegeben.") # nur Kritik abgeben, wenn noch nicht erstellt
else:
QuizCriticism.objects.create(
question=question,
id_question=question_id,
quiz=quiz,
user=request.user,
question_criticism=question_text,
criticism=CRITICISM_MAP[critic_type],
)
def delete_critique(request,critique_id,quiz_id):
critique = get_object_or_404(QuizCriticism, id=critique_id)
critique.delete()
return redirect('library:detail_quiz', pk=quiz_id)
def get_critisized_quizzes():
critisized_quizzes = QivipQuiz.objects.annotate(
criticism_count=Count('criticism_quiz')
).filter(criticism_count__gt=0)
return critisized_quizzes
def overview_quiz(request, connect="all"):
critisized_quizzes = get_critisized_quizzes()
form = QuizFilterForm(request.GET)
user = request.user if request.user.is_authenticated else None
@@ -502,6 +278,7 @@ def overview_quiz(request, connect="all"):
favorite_quizzes = paginator_fav.page(1)
context = {
'critisized_quizzes':critisized_quizzes,
'form': form,
'my_quizzes': my_quizzes,
'all_quizzes': all_quizzes,
@@ -552,117 +329,6 @@ def overview_quiz_my(request):
def overview_quiz_favorites(request):
return overview_quiz(request, connect="favorite")
"""
def overview_quiz(request):
# Filter
form = QuizFilterForm(request.GET)
try:
favorite_quizzes_data = QivipQuizFavorite.objects.filter(user=request.user, favorite=True).order_by("-favorite_date")
favorite_quiz_ids = favorite_quizzes_data.values_list('quiz', flat=True)
favorite_quizzes = QivipQuiz.objects.filter(id__in=favorite_quiz_ids)
except:
favorite_quizzes_data = QivipQuizFavorite.objects.none()
favorite_quizzes = QivipQuizFavorite.objects.none()
favorite_quiz_ids = favorite_quizzes.none()
favorite_quizzes = QivipQuiz.objects.none()
# Initialize querysets
if request.user.is_authenticated:
filter_my_quizzes = QivipQuiz.objects.filter(user_id=request.user).annotate(max_amount_questions=Count('questions'))
favorite_quizzes = favorite_quizzes.filter(id__in=favorite_quiz_ids).annotate(max_amount_questions=Count('questions'))
filter_all_quizzes = QivipQuiz.objects.exclude(user_id=request.user)
else:
favorite_quizzes = QivipQuiz.objects.none()
filter_my_quizzes = QivipQuiz.objects.none()
filter_all_quizzes = QivipQuiz.objects.all()
# Apply common filters to all quizzes
filter_all_quizzes = filter_all_quizzes.annotate(max_amount_questions=Count('questions'))
filter_all_quizzes = filter_all_quizzes.filter(max_amount_questions__gte=1, status='öffentlich')
if form.is_valid():
search = form.cleaned_data.get('search')
username= form.cleaned_data.get('user')
max_amount_questions= form.cleaned_data.get('max_amount_questions')
min_amount_questions= form.cleaned_data.get('min_amount_questions')
filters = {}
if username:
try:
user = User.objects.get(username=username)
filters['user_id'] = user.id
except User.DoesNotExist:
favorite_quizzes = QivipQuiz.objects.none()
filter_my_quizzes = QivipQuiz.objects.none()
filter_all_quizzes=QivipQuiz.objects.none()
if search:
filters['name__icontains'] = search
if min_amount_questions:
filters['max_amount_questions__gte'] = min_amount_questions
if max_amount_questions:
filters['max_amount_questions__lte'] = max_amount_questions
try:
filter_all_quizzes = filter_all_quizzes.filter(**filters)
except:
filter_all_quizzes=QivipQuiz.objects.none()
try:
filter_my_quizzes = filter_my_quizzes.filter(**filters)
except:
filter_my_quizzes=QivipQuiz.objects.none()
try:
favorite_quizzes = favorite_quizzes.filter(**filters)
except:
favorite_quizzes=QivipQuiz.objects.none()
# 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 all quizzes
all_quizzes_paginator = Paginator(filter_all_quizzes.order_by('-creation_date'), 8) # 8 quizzes per page
try:
all_quizzes = all_quizzes_paginator.page(request.GET.get('all_page', 1))
except:
all_quizzes = all_quizzes_paginator.page(1)
try:
favorite_quizzes = favorite_quizzes.filter(
favorites__user=request.user, # Filtere nach dem User
favorites__favorite=True # Filtere nach favorisierten Quizzes
).order_by('-favorites__favorite_date')
except:
pass
# Pagination for favorite_quizzes
favorite_quizzes_paginator = Paginator(favorite_quizzes, 8) # 9 quizzes per page
try:
favorite_quizzes = favorite_quizzes_paginator.page(request.GET.get('favorite_page', 1))
except:
favorite_quizzes = favorite_quizzes_paginator.page(1)
context = {
'show_search': True,
'favorite_quiz_ids': favorite_quiz_ids,
'favorite_quizzes':favorite_quizzes,
'my_quizzes': my_quizzes,
'all_quizzes': all_quizzes,
'form': form,
}
return render(request, 'library/overview_quiz.html', context)
"""
@login_required
def favorite_quiz(request, pk):
@@ -802,8 +468,11 @@ def delete_quiz(request, pk):
return redirect('library:overview_quiz')
return render(request, 'library/delete_confirmation.html', {'object': quiz})
# Quiz anzeigen
def detail_quiz(request, pk):
critisize_quiz(request, pk)
quiz = get_object_or_404(QivipQuiz, pk=pk)
show_answers = request.GET.get('show_answers', 'false').lower() == 'true'
questions = QivipQuestion.objects.filter(quiz_id=quiz)