From 736db1c480ca4df2409245a6c7789224548a3989 Mon Sep 17 00:00:00 2001 From: ben8 Date: Sun, 13 Jul 2025 13:12:45 +0200 Subject: [PATCH] PDF --- django/library/urls.py | 2 + django/library/views.py | 323 ++++++++++++++++++++++ django/templates/library/detail_quiz.html | 39 ++- requirements.txt | 3 +- 4 files changed, 363 insertions(+), 4 deletions(-) diff --git a/django/library/urls.py b/django/library/urls.py index fa19cf3..f9cc6bf 100644 --- a/django/library/urls.py +++ b/django/library/urls.py @@ -20,5 +20,7 @@ urlpatterns = [ path('detail/copy//', views.copy_quiz, name='copy_quiz'), path('favorite_quiz//', views.favorite_quiz, name='favorite_quiz'), path("quiz-names-json/", views.quiz_names_json, name="quiz_names_json"), + path('quiz//pdf/', views.pdf_solution, name='quiz_solution'), + path('quiz//pdf/task', views.pdf_task, name='quiz_task'), ] diff --git a/django/library/views.py b/django/library/views.py index 2620220..5fb7109 100644 --- a/django/library/views.py +++ b/django/library/views.py @@ -17,6 +17,329 @@ from django.contrib.auth.models import User from urllib.parse import urlparse, urlunparse # Übersicht aller Quizze +from reportlab.lib.pagesizes import A4 +from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle +from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle +from reportlab.lib import colors +from django.http import HttpResponse +import json +from datetime import datetime +from reportlab.platypus import Image + +from reportlab.lib import colors +from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, HRFlowable +from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle +from reportlab.lib.pagesizes import A4 +from django.http import HttpResponse +from django.shortcuts import get_object_or_404 +import json +from io import BytesIO +import requests +from django.core.files.storage import default_storage +from PIL import Image as PILImage, ImageOps +from django.conf import settings +import os +from django.utils.html import escape + +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'{escaped_url}' + 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'{escaped_url}' + story.append(Paragraph(link_html, styles['Normal'])) + doc.build(story) + return response + + + def quiz_names_json(request): form = QuizFilterForm(request.GET) diff --git a/django/templates/library/detail_quiz.html b/django/templates/library/detail_quiz.html index 20deecb..fe6dd3e 100644 --- a/django/templates/library/detail_quiz.html +++ b/django/templates/library/detail_quiz.html @@ -3,7 +3,6 @@ {% block content %}
-

{{ quiz.name }}

@@ -50,9 +49,10 @@
+ - + {% if quiz.description %}

{{ quiz.description }}

{% endif %} @@ -253,6 +253,38 @@ {% endif %} + {% if questions %} + + {% endif %}

Credits:

{% if quiz.credits %} @@ -266,6 +298,8 @@
+ +
    {% regroup questions by credits as grouped_credits %} @@ -284,7 +318,6 @@
- {% endblock %} diff --git a/requirements.txt b/requirements.txt index 9442152..7c74c1d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,4 +8,5 @@ django-cleanup redis channels_redis python-dotenv # Production server safety -qrcode~=7.4.2 \ No newline at end of file +qrcode~=7.4.2 +reportlab~=4.2.0 \ No newline at end of file