diff --git a/aleksis/core/assets/components/SmallContainer.vue b/aleksis/core/assets/components/SmallContainer.vue new file mode 100644 index 0000000000000000000000000000000000000000..01d198ef7ac58b7ac1cb1f630b93c50f02644a77 --- /dev/null +++ b/aleksis/core/assets/components/SmallContainer.vue @@ -0,0 +1,15 @@ +<template> + <v-row> + <v-col sm="0" md="1" lg="2" xl="3" /> + <v-col sm="12" md="10" lg="8" xl="6"> + <slot></slot> + </v-col> + <v-col sm="0" md="1" lg="2" xl="3" /> + </v-row> +</template> + +<script> +export default { + name: "SmallContainer", +}; +</script> diff --git a/aleksis/core/assets/components/celery_progress/CeleryProgress.vue b/aleksis/core/assets/components/celery_progress/CeleryProgress.vue index 6de627139e71b404da4eea976fcd749db89cc1dd..144a2a7628e91bd60b060409e88c745d8b2683ee 100644 --- a/aleksis/core/assets/components/celery_progress/CeleryProgress.vue +++ b/aleksis/core/assets/components/celery_progress/CeleryProgress.vue @@ -1,86 +1,82 @@ <template> - <v-row> - <v-col sm="0" md="1" lg="2" xl="3" /> - <v-col sm="12" md="10" lg="8" xl="6"> - <v-card :loading="$apollo.loading"> - <v-card-title v-if="progress"> - {{ progress.meta.title }} - </v-card-title> - <v-card-text v-if="progress"> - <v-progress-linear - :value="progress.progress.percent" - buffer-value="0" - color="primary" - class="mb-2" - stream - /> - <div class="text-center mb-4"> - {{ - progress.meta.progressTitle - ? progress.meta.progressTitle - : $t("celery_progress.progress_title") - }} - </div> - <div v-if="progress"> - <message-box - v-for="(message, idx) in progress.messages" - dense - :type="message.tag" - transition="slide-x-transition" - :key="idx" - > - {{ message.message }} - </message-box> - </div> + <small-container> + <v-card :loading="$apollo.loading"> + <v-card-title v-if="progress"> + {{ progress.meta.title }} + </v-card-title> + <v-card-text v-if="progress"> + <v-progress-linear + :value="progress.progress.percent" + buffer-value="0" + color="primary" + class="mb-2" + stream + /> + <div class="text-center mb-4"> + {{ + progress.meta.progressTitle + ? progress.meta.progressTitle + : $t("celery_progress.progress_title") + }} + </div> + <div v-if="progress"> <message-box - v-if="progress.state === 'ERROR'" + v-for="(message, idx) in progress.messages" dense - type="error" + :type="message.tag" transition="slide-x-transition" + :key="idx" > - {{ - progress.meta.errorMessage - ? progress.meta.errorMessage - : $t("celery_progress.error_message") - }} + {{ message.message }} </message-box> - <message-box - v-if="progress.state === 'SUCCESS'" - dense - type="success" - transition="slide-x-transition" - > - {{ - progress.meta.successMessage - ? progress.meta.successMessage - : $t("celery_progress.success_message") - }} - </message-box> - </v-card-text> - <v-card-actions - v-if=" - progress && - (progress.state === 'ERROR' || progress.state === 'SUCCESS') - " + </div> + <message-box + v-if="progress.state === 'ERROR'" + dense + type="error" + transition="slide-x-transition" > - <back-button :href="progress.meta.backUrl" text /> - <v-spacer /> - <v-btn - v-if="progress.meta.additionalButton" - :href="progress.meta.additionalButton.url" - text - color="primary" - > - <v-icon v-if="progress.meta.additionalButton.icon" left> - {{ progress.meta.additionalButton.icon }} - </v-icon> - {{ progress.meta.additionalButton.title }} - </v-btn> - </v-card-actions> - </v-card> - </v-col> - <v-col sm="0" md="1" lg="2" xl="3" /> - </v-row> + {{ + progress.meta.errorMessage + ? progress.meta.errorMessage + : $t("celery_progress.error_message") + }} + </message-box> + <message-box + v-if="progress.state === 'SUCCESS'" + dense + type="success" + transition="slide-x-transition" + > + {{ + progress.meta.successMessage + ? progress.meta.successMessage + : $t("celery_progress.success_message") + }} + </message-box> + </v-card-text> + <v-card-actions + v-if=" + progress && + (progress.state === 'ERROR' || progress.state === 'SUCCESS') + " + > + <back-button :href="progress.meta.backUrl" text /> + <v-spacer /> + <v-btn + v-if="progress.meta.additionalButton" + :href="progress.meta.additionalButton.url" + text + color="primary" + > + <v-icon v-if="progress.meta.additionalButton.icon" left> + {{ progress.meta.additionalButton.icon }} + </v-icon> + {{ progress.meta.additionalButton.title }} + </v-btn> + </v-card-actions> + </v-card> + </small-container> </template> <script> @@ -88,10 +84,11 @@ import BackButton from "../BackButton.vue"; import MessageBox from "../MessageBox.vue"; import gqlCeleryProgress from "./celeryProgress.graphql"; import gqlCeleryProgressFetched from "./celeryProgressFetched.graphql"; +import SmallContainer from "../SmallContainer.vue"; export default { name: "CeleryProgress", - components: { BackButton, MessageBox }, + components: { SmallContainer, BackButton, MessageBox }, apollo: { celeryProgressByTaskId: { query: gqlCeleryProgress, diff --git a/aleksis/core/assets/components/pdf/DownloadPDF.vue b/aleksis/core/assets/components/pdf/DownloadPDF.vue new file mode 100644 index 0000000000000000000000000000000000000000..8438a01ee48c0e8b72cc14997cc5f5d0727b639d --- /dev/null +++ b/aleksis/core/assets/components/pdf/DownloadPDF.vue @@ -0,0 +1,48 @@ +<template> + <small-container> + <v-card> + <v-card-title v-if="pdf">{{ $t("download_pdf.title") }}</v-card-title> + <v-card-text v-if="pdf" class="body-1"> + {{ $t("download_pdf.notice") }} + </v-card-text> + <v-card-actions v-if="pdf"> + <v-btn color="primary" text :href="pdf.file.url" download> + <v-icon left>mdi-download</v-icon> + {{ $t("download_pdf.download") }} + </v-btn> + </v-card-actions> + <v-skeleton-loader + type="article" + v-if="$apollo.queries.pdf.loading" + ></v-skeleton-loader> + </v-card> + </small-container> +</template> + +<script> +import gqlPdf from "./pdf.graphql"; +import SmallContainer from "../SmallContainer.vue"; + +export default { + name: "DownloadPDF", + components: { SmallContainer }, + apollo: { + pdf: { + query: gqlPdf, + variables() { + return { + id: this.$route.params.id, + }; + }, + }, + }, + watch: { + pdf(value) { + // Automatic redirect + if (value) { + window.location.href = value.file.url; + } + }, + }, +}; +</script> diff --git a/aleksis/core/assets/components/pdf/pdf.graphql b/aleksis/core/assets/components/pdf/pdf.graphql new file mode 100644 index 0000000000000000000000000000000000000000..aac3228d75c3c77ab131500b5e0d5e2ae1b8f503 --- /dev/null +++ b/aleksis/core/assets/components/pdf/pdf.graphql @@ -0,0 +1,7 @@ +query ($id: ID!) { + pdf: pdfById(id: $id) { + file { + url + } + } +} diff --git a/aleksis/core/assets/messages/de.json b/aleksis/core/assets/messages/de.json index 435dd4f8a9618f0ac135d81ebf2c423b1e1a77d1..32b9368e9d2e93f99bd8680909c07a709ed6d64c 100644 --- a/aleksis/core/assets/messages/de.json +++ b/aleksis/core/assets/messages/de.json @@ -93,6 +93,11 @@ "data_check": { "menu_title": "Datenprüfungen" }, + "download_pdf": { + "title": "PDF-Datei wird heruntergeladen ...", + "download": "Herunterladen", + "notice": "Wenn der Download nicht automatisch beginnt, klicken Sie bitte auf den Button unten." + }, "group": { "additional_field": { "menu_title": "Zusätzliche Felder", diff --git a/aleksis/core/assets/messages/en.json b/aleksis/core/assets/messages/en.json index 8da883db7a5ef7ff60028c5a20a319d9f5afad97..b2679b64a49563ff4a9491cfe891ef4d6ecfe6f5 100644 --- a/aleksis/core/assets/messages/en.json +++ b/aleksis/core/assets/messages/en.json @@ -82,6 +82,11 @@ "running_tasks": "1 running task | {number} running tasks", "success_message": "The operation has been finished successfully." }, + "download_pdf": { + "title": "Downloading PDF file ...", + "download": "Download", + "notice": "If the download does not start automatically, please click the button below." + }, "dashboard": { "dashboard_widget": { "menu_title": "Dashboard Widgets", diff --git a/aleksis/core/assets/routes.js b/aleksis/core/assets/routes.js index cc869b32a1788d63f3795ca8180bd7d6ea07a0ad..24dcb93d146afaedd1b27a74f4750339b1ac7d93 100644 --- a/aleksis/core/assets/routes.js +++ b/aleksis/core/assets/routes.js @@ -733,8 +733,8 @@ const routes = [ name: "core.testPdf", }, { - path: "/pdfs/:pk(\\d+)/", - component: () => import("./components/LegacyBaseTemplate.vue"), + path: "/pdfs/:id", + component: () => import("./components/pdf/DownloadPDF.vue"), name: "core.redirectToPdfUrl", }, { diff --git a/aleksis/core/models.py b/aleksis/core/models.py index fff6d2cfa71a70b30d2f92a39f782ffc151c6927..e2f348a7ac9becd40cbc64bae7194b6139eba305 100644 --- a/aleksis/core/models.py +++ b/aleksis/core/models.py @@ -4,7 +4,7 @@ import hmac import uuid from datetime import date, datetime, timedelta from typing import Any, Iterable, List, Optional, Sequence, Union -from urllib.parse import urlparse +from urllib.parse import urljoin, urlparse from django.conf import settings from django.contrib.auth import get_user_model @@ -1316,7 +1316,7 @@ class TaskUserAssignment(ExtensibleModel): # Task not yet finished return - link = reverse("task_status", args=[self.task_result.task_id]) + link = urljoin(settings.BASE_URL, self.get_absolute_url()) notification = Notification( sender=_("Background task"), @@ -1329,6 +1329,9 @@ class TaskUserAssignment(ExtensibleModel): notification.save() return notification + def get_absolute_url(self) -> str: + return f"/celery_progress/{self.task_result.task_id}" + class Meta: verbose_name = _("Task user assignment") verbose_name_plural = _("Task user assignments") diff --git a/aleksis/core/schema/__init__.py b/aleksis/core/schema/__init__.py index dd85daf36caeb501ae3e4d9bc4709149f8a13098..71543acf5ef33020da44bbd6266db692a02d123f 100644 --- a/aleksis/core/schema/__init__.py +++ b/aleksis/core/schema/__init__.py @@ -9,7 +9,7 @@ from haystack.inputs import AutoQuery from haystack.query import SearchQuerySet from haystack.utils.loading import UnifiedIndex -from ..models import CustomMenu, Notification, Person, TaskUserAssignment +from ..models import CustomMenu, Notification, PDFFile, Person, TaskUserAssignment from ..util.apps import AppConfig from ..util.core_helpers import get_allowed_object_ids, get_app_module, get_app_packages, has_person from .celery_progress import CeleryProgressFetchedMutation, CeleryProgressType @@ -18,6 +18,7 @@ from .group import GroupType # noqa from .installed_apps import AppType from .message import MessageType from .notification import MarkNotificationReadMutation, NotificationType +from .pdf import PDFFileType from .permissions import GlobalPermissionType from .person import PersonMutation, PersonType from .school_term import SchoolTermType # noqa @@ -43,6 +44,8 @@ class Query(graphene.ObjectType): celery_progress_by_task_id = graphene.Field(CeleryProgressType, task_id=graphene.String()) celery_progress_by_user = graphene.List(CeleryProgressType) + pdf_by_id = graphene.Field(PDFFileType, id=graphene.ID()) + search_snippets = graphene.List( SearchResultType, query=graphene.String(), limit=graphene.Int(required=False) ) @@ -111,6 +114,12 @@ class Query(graphene.ObjectType): if task.get_progress_with_meta()["complete"] is False ] + def resolve_pdf_by_id(root, info, id, **kwargs): # noqa + pdf_file = PDFFile.objects.get(pk=id) + if has_person(info.context) and not info.context.user.person == pdf_file.person: + raise PermissionDenied() + return pdf_file + def resolve_search_snippets(root, info, query, limit=-1, **kwargs): indexed_models = UnifiedIndex().get_indexed_models() allowed_object_ids = get_allowed_object_ids(info.context.user, indexed_models) diff --git a/aleksis/core/schema/base.py b/aleksis/core/schema/base.py index 363c6b8ac849affcba2d9c1cbf9eefa74498121d..e927dadccaaab7b2bf39c2110286970222f70f5b 100644 --- a/aleksis/core/schema/base.py +++ b/aleksis/core/schema/base.py @@ -1,3 +1,4 @@ +import graphene from graphene_django import DjangoObjectType from ..util.core_helpers import queryset_rules_filter @@ -12,3 +13,14 @@ class RulesObjectType(DjangoObjectType): q = super().get_queryset(queryset, info) return queryset_rules_filter(info.context, q, perm) + + +class FieldFileType(graphene.ObjectType): + url = graphene.String() + absolute_url = graphene.String() + + def resolve_url(root, info, **kwargs): + return root.url if root else "" + + def resolve_absolute_url(root, info, **kwargs): + return info.context.build_absolute_uri(root.url) if root else "" diff --git a/aleksis/core/schema/pdf.py b/aleksis/core/schema/pdf.py new file mode 100644 index 0000000000000000000000000000000000000000..1a3a3689b78849301710cd25e362313f71a7e308 --- /dev/null +++ b/aleksis/core/schema/pdf.py @@ -0,0 +1,14 @@ + +import graphene +from graphene_django import DjangoObjectType + +from ..models import PDFFile +from .base import FieldFileType + + +class PDFFileType(DjangoObjectType): + file = graphene.Field(FieldFileType) + + class Meta: + model = PDFFile + exclude = ["html_file"] diff --git a/aleksis/core/schema/person.py b/aleksis/core/schema/person.py index 6c5950cdcb1f9a147f5145559d4c9c183111d2c1..e64082f6876cf926ece10099cafbae639ffaf02f 100644 --- a/aleksis/core/schema/person.py +++ b/aleksis/core/schema/person.py @@ -10,16 +10,10 @@ from graphene_django.forms.mutation import DjangoModelFormMutation from ..forms import PersonForm from ..models import DummyPerson, Person from ..util.core_helpers import get_site_preferences, is_impersonate +from .base import FieldFileType from .notification import NotificationType -class FieldFileType(graphene.ObjectType): - url = graphene.String() - - def resolve_url(root, info, **kwargs): - return root.url if root else "" - - class PersonPreferencesType(graphene.ObjectType): theme_design_mode = graphene.String() diff --git a/aleksis/core/urls.py b/aleksis/core/urls.py index c526d00c44ee11244cdfc143c4d7de3800dfd29a..a8cee0d037d7b06540a224a7a69c93f781fd74d2 100644 --- a/aleksis/core/urls.py +++ b/aleksis/core/urls.py @@ -374,9 +374,6 @@ urlpatterns = [ views.AssignPermissionView.as_view(), name="assign_permission", ), - path( - "pdfs/<int:pk>", views.RedirectToPDFFile.as_view(), name="redirect_to_pdf_file" - ), path("ical/", views.ICalFeedListView.as_view(), name="ical_feed_list"), path("ical/create/", views.ICalFeedCreateView.as_view(), name="ical_feed_create"), path( diff --git a/aleksis/core/util/celery_progress.py b/aleksis/core/util/celery_progress.py index d377d9d334c0243af210cc68e3474aeaea748b2e..008b979ec37207d05b2b22538a7761591ed8b22c 100644 --- a/aleksis/core/util/celery_progress.py +++ b/aleksis/core/util/celery_progress.py @@ -4,8 +4,7 @@ from typing import Callable, Generator, Iterable, Optional, Sequence, Union from django.apps import apps from django.contrib import messages -from django.http import HttpRequest -from django.shortcuts import redirect +from django.http import HttpRequest, HttpResponseRedirect from celery.result import AsyncResult from celery_progress.backend import PROGRESS_STATE, AbstractProgressRecorder @@ -220,4 +219,4 @@ def render_progress_page( assignment.additional_button_icon = button_icon or "" assignment.save() - return redirect("task_status", task_id=task_result.task_id) + return HttpResponseRedirect(request.build_absolute_uri(assignment.get_absolute_url())) diff --git a/aleksis/core/util/pdf.py b/aleksis/core/util/pdf.py index 66b5e1a9fdf58d4630651e1795167df001f42567..01bf7601c92cc81425f04268a6276509ea4ab4f5 100644 --- a/aleksis/core/util/pdf.py +++ b/aleksis/core/util/pdf.py @@ -25,7 +25,7 @@ from selenium import webdriver from aleksis.core.celery import app from aleksis.core.models import PDFFile from aleksis.core.util.celery_progress import recorded_task, render_progress_page -from aleksis.core.util.core_helpers import process_custom_context_processors +from aleksis.core.util.core_helpers import has_person, process_custom_context_processors def _generate_pdf_with_chromium(temp_dir, pdf_path, html_url, lang): @@ -96,6 +96,8 @@ def generate_pdf_from_html( # In some cases, the file object is already created (to get a redirect URL for the PDF) if not file_object: file_object = PDFFile.objects.create() + if request and has_person(request): + file_object.person = request.user.person file_object.html_file = html_file file_object.save() @@ -141,7 +143,7 @@ def render_pdf( file_object, result = generate_pdf_from_template(template_name, context, request) - redirect_url = reverse("redirect_to_pdf_file", args=[file_object.pk]) + redirect_url = f"/pdfs/{file_object.pk}" return render_progress_page( request, diff --git a/aleksis/core/views.py b/aleksis/core/views.py index 8251a482707d5b07c787fb1c63d923cf348f86da..29d5ca002594e9ea9a989330c66d239e3eb856b3 100644 --- a/aleksis/core/views.py +++ b/aleksis/core/views.py @@ -99,7 +99,6 @@ from .models import ( Group, GroupType, OAuthApplication, - PDFFile, Person, PersonalICalUrl, PersonInvitation, @@ -1318,18 +1317,6 @@ class OAuth2RegisterView(PermissionRequiredMixin, AdvancedCreateView): form_class = OAuthApplicationForm -class RedirectToPDFFile(SingleObjectMixin, View): - """Redirect to a generated PDF file.""" - - model = PDFFile - - def get(self, *args, **kwargs): - file_object = self.get_object() - if not file_object.file: - raise Http404(_("The requested PDF file does not exist")) - return redirect(file_object.file.url) - - class CustomPasswordChangeView(LoginRequiredMixin, PermissionRequiredMixin, PasswordChangeView): """Custom password change view to allow to disable changing of password."""