From b81c52ccfeba18d5db4b959cb7e0c1b75b03ea91 Mon Sep 17 00:00:00 2001 From: Steve Yonkeu Date: Mon, 15 Jan 2024 05:49:58 +0000 Subject: [PATCH 01/10] feat: adds weasy to requirements --- zubhub_backend/Makefile | 2 +- .../compose/celery/requirements.txt | 1 + zubhub_backend/compose/web/requirements.txt | 1 + zubhub_backend/zubhub/activities/urls.py | 3 +- zubhub_backend/zubhub/activities/views.py | 39 +++++++++++++++++-- .../activities/activity_download.html | 0 6 files changed, 40 insertions(+), 6 deletions(-) create mode 100644 zubhub_backend/zubhub/templates/activities/activity_download.html diff --git a/zubhub_backend/Makefile b/zubhub_backend/Makefile index e8e2ad446..49273425e 100644 --- a/zubhub_backend/Makefile +++ b/zubhub_backend/Makefile @@ -98,7 +98,7 @@ default-theme: docker-compose -f docker-compose.yml exec web bash -c "python zubhub/manage.py create_default_theme" .PHONY: default-theme -init: .env start migrate admin-user add-theme ## Initialize docker-compose containers +init: .env start migrate admin-user default-theme ## Initialize docker-compose containers .PHONY: init search-index: diff --git a/zubhub_backend/compose/celery/requirements.txt b/zubhub_backend/compose/celery/requirements.txt index 4dc0eea12..c8adf459e 100644 --- a/zubhub_backend/compose/celery/requirements.txt +++ b/zubhub_backend/compose/celery/requirements.txt @@ -82,6 +82,7 @@ uritemplate>=3.0.1 urllib3>=1.25.11 vine>=1.3.0 watchdog>=0.10.2 +weasyprint>=60.2 wcwidth>=0.2.5 whitenoise>=4.1.4 django-extensions>=1.0.0 diff --git a/zubhub_backend/compose/web/requirements.txt b/zubhub_backend/compose/web/requirements.txt index dacef1dc8..67f934862 100644 --- a/zubhub_backend/compose/web/requirements.txt +++ b/zubhub_backend/compose/web/requirements.txt @@ -85,5 +85,6 @@ uritemplate>=3.0.1 urllib3>=1.25.11 vine>=1.3.0 watchdog>=0.10.2 +weasyprint>=60.2 wcwidth>=0.2.5 whitenoise>=4.1.4 diff --git a/zubhub_backend/zubhub/activities/urls.py b/zubhub_backend/zubhub/activities/urls.py index 927aa0e13..9397357e0 100644 --- a/zubhub_backend/zubhub/activities/urls.py +++ b/zubhub_backend/zubhub/activities/urls.py @@ -12,5 +12,6 @@ path('/delete/', ActivityDeleteAPIView.as_view(), name='delete'), path('/toggle-save/', ToggleSaveAPIView.as_view(), name='save'), path('/toggle-publish/', togglePublishActivityAPIView.as_view(), name='publish'), - path('/', ActivityDetailsAPIView.as_view(), name='detail_activity') + path('/', ActivityDetailsAPIView.as_view(), name='detail_activity'), + path('/pdf/', DownloadActivityPDF.as_view(), name='pdf'), ] diff --git a/zubhub_backend/zubhub/activities/views.py b/zubhub_backend/zubhub/activities/views.py index 7a7f015b7..30a18efb3 100644 --- a/zubhub_backend/zubhub/activities/views.py +++ b/zubhub_backend/zubhub/activities/views.py @@ -1,7 +1,6 @@ -from django.shortcuts import render -from django.utils.translation import ugettext_lazy as _ -from rest_framework.decorators import api_view -from rest_framework.response import Response +from django.http import HttpResponse +from django.template.loader import render_to_string +from weasyprint import HTML from rest_framework.generics import ( ListAPIView, CreateAPIView, RetrieveAPIView, UpdateAPIView, DestroyAPIView) from rest_framework.permissions import IsAuthenticated, IsAuthenticatedOrReadOnly, AllowAny @@ -182,3 +181,35 @@ def get_object(self): obj.publish = not obj.publish obj.save() return obj + + + +class DownloadActivityPDF(RetrieveAPIView): + """ + Download an activities. + Requires activities id. + Returns activities file. + """ + queryset = Activity.objects.all() + permission_classes = [IsAuthenticated] + serializer_class = ActivitySerializer + + def get_object(self): + pk = self.kwargs.get("pk") + obj = get_object_or_404(self.get_queryset(), pk=pk) + return obj + + def get(self, request, *args, **kwargs): + activity = self.get_object() + + context = { + 'activity': activity, + } + html_string = render_to_string('activities/activity_download.html', context) + + html = HTML(string=html_string) + pdf = html.write_pdf() + + response = HttpResponse(pdf, content_type='application/pdf') + response['Content-Disposition'] = f'filename="{activity.id}.pdf"' + return response diff --git a/zubhub_backend/zubhub/templates/activities/activity_download.html b/zubhub_backend/zubhub/templates/activities/activity_download.html new file mode 100644 index 000000000..e69de29bb From a14712a5828a600f710c935bdd9014c827fa7660 Mon Sep 17 00:00:00 2001 From: Steve Yonkeu Date: Mon, 15 Jan 2024 08:07:46 +0000 Subject: [PATCH 02/10] feat: translates download button --- zubhub_frontend/zubhub/public/locales/en/translation.json | 4 ++++ zubhub_frontend/zubhub/public/locales/hi/translation.json | 8 ++++++-- .../src/views/activity_details/ActivityDetailsV2.jsx | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/zubhub_frontend/zubhub/public/locales/en/translation.json b/zubhub_frontend/zubhub/public/locales/en/translation.json index 51afb1356..e9bd0e3a4 100644 --- a/zubhub_frontend/zubhub/public/locales/en/translation.json +++ b/zubhub_frontend/zubhub/public/locales/en/translation.json @@ -1084,6 +1084,10 @@ "mediaServerError": "Sorry media server is down we couldn't upload your files! try again later", "uploadError": "error occurred while downloading file : " } + }, + "downloadButton": { + "downloading": "Downloading...", + "download": "Download PDF" } }, diff --git a/zubhub_frontend/zubhub/public/locales/hi/translation.json b/zubhub_frontend/zubhub/public/locales/hi/translation.json index 03af477a0..f9e7c7af2 100644 --- a/zubhub_frontend/zubhub/public/locales/hi/translation.json +++ b/zubhub_frontend/zubhub/public/locales/hi/translation.json @@ -825,7 +825,7 @@ } }, "actions": { - "submit": "परिवर्तनों को सुरक्षित करें", + "submit": "परिवर्तनों को सुरक्षित करें" }, "delete": { "label": "टीम हटाएं", @@ -986,7 +986,11 @@ "mediaServerError": "क्षमा करें मीडिया सर्वर डाउन है हम आपकी फ़ाइलें अपलोड नहीं कर सके! बाद में पुन: प्रयास", "uploadError": "फ़ाइल डाउनलोड करते समय त्रुटि हुई: " } - } + }, + "downloadButton": { + "downloading": "डाउनलोड हो रहा है...", + "download": "डाउनलोड पीडीऍफ़" + } }, "activityDetails": { diff --git a/zubhub_frontend/zubhub/src/views/activity_details/ActivityDetailsV2.jsx b/zubhub_frontend/zubhub/src/views/activity_details/ActivityDetailsV2.jsx index c2ab283cc..f6ebf9cc6 100644 --- a/zubhub_frontend/zubhub/src/views/activity_details/ActivityDetailsV2.jsx +++ b/zubhub_frontend/zubhub/src/views/activity_details/ActivityDetailsV2.jsx @@ -148,7 +148,7 @@ export default function ActivityDetailsV2(props) { primaryButtonOutlinedStyle style={{ borderRadius: 4 }} > - {isDownloading ? 'Downloading...' : 'Download PDF'} + {isDownloading ? t('activities.downloadButton.downloading') : t('activities.downloadButton.download')} From dd7f5b376bd21857b7fcb4a15ad30a770fea83dc Mon Sep 17 00:00:00 2001 From: Steve Yonkeu Date: Fri, 19 Jan 2024 12:23:19 +0100 Subject: [PATCH 03/10] feat: add utils for qr code and pdf rendering --- zubhub_backend/zubhub/activities/utils.py | 64 +++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/zubhub_backend/zubhub/activities/utils.py b/zubhub_backend/zubhub/activities/utils.py index 21e4d8014..5ac8b56ff 100644 --- a/zubhub_backend/zubhub/activities/utils.py +++ b/zubhub_backend/zubhub/activities/utils.py @@ -1,3 +1,9 @@ +import io +import base64 +import qrcode +from django.http import HttpResponse +from django.template.loader import get_template +from weasyprint import HTML from .models import * @@ -67,3 +73,61 @@ def update_making_steps(activity, making_steps): def update_inspiring_examples(activity, inspiring_examples): InspiringExample.objects.filter(activity=activity).delete() create_inspiring_examples(activity, inspiring_examples) + + +def generate_qr_code(link): + """ + Generate a QR code for a given link and return it as a base64 string. + + Args: + link (str): The link to encode in the QR code. + + Returns: + str: The QR code as a base64 string. + """ + # Generate QR code + qr = qrcode.QRCode( + version=1, + error_correction=qrcode.constants.ERROR_CORRECT_L, + box_size=10, + border=4, + ) + qr.add_data(link) + qr.make(fit=True) + + img = qr.make_image(fill_color="black", back_color="white") + + buf = io.BytesIO() + img.save(buf, format="PNG") + + img_bytes = buf.getvalue() + + img_base64 = base64.b64encode(img_bytes).decode() + + return img_base64 + + +def generate_pdf(template_path, context): + """ + Generate a PDF file from a Jinja template. + + Args: + template_path (str): The file path to the Jinja template. + context (dict): The context data for rendering the template. + + Returns: + HttpResponse: A Django HTTP response with the generated PDF. + """ + template = get_template(template_path) + + html = template.render(context) + + pdf = HTML(string=html).write_pdf() + + activity_id = context['activity_id'] + + response = HttpResponse(pdf, content_type="application/pdf") + response["Content-Disposition"] = f'attachment; filename="{activity_id}.pdf"' + + return response + From 292727af63ffba0368d4411448ed74f6a59d011f Mon Sep 17 00:00:00 2001 From: Steve Yonkeu Date: Fri, 19 Jan 2024 12:24:05 +0100 Subject: [PATCH 04/10] feat: implements template for pdf --- .../activities/activity_download.html | 317 ++++++++++++++++++ 1 file changed, 317 insertions(+) diff --git a/zubhub_backend/zubhub/templates/activities/activity_download.html b/zubhub_backend/zubhub/templates/activities/activity_download.html index e69de29bb..71d002344 100644 --- a/zubhub_backend/zubhub/templates/activities/activity_download.html +++ b/zubhub_backend/zubhub/templates/activities/activity_download.html @@ -0,0 +1,317 @@ + + + + + + + + +
+
+
+ {{ activity.title }} +
+
+ {% if creators %} + {% for creator in creators %} + {{ creator.username }} avatar + {{ creator.username }} + {% endfor %} + {% endif %} +
+
+
+

+ Introduction +

+
+ {{ activity.introduction | safe }} +
+
+ + + +
+
+
+

+ Category +

+
+ {% for category in activity_category %} + {{ category }} + {% endfor %} +
+
+
+

+ Class Grade +

+
+ {{ activity.class_grade }} +
+
+ {% if activity.materials_used %} +
+

Material Used

+
+ {{ activity.materials_used | safe }} +
+
+ + + +
+
+ {% endif %} + {% if activity_steps %} + {% for step in activity_steps %} +
+

+ Step {{ step.step_order }}: {{ step.title }} +

+
+ {{ step.description | safe }} +
+
+ {% for img in activity_steps_images %} + {{ img.file_url }} + + Activity image + + {% endfor %} +
+
+ {% endfor %} + {% endif %} +
+ + + From f7716a5cb51a13ad099980562388029553e81390 Mon Sep 17 00:00:00 2001 From: Steve Yonkeu Date: Fri, 19 Jan 2024 12:25:17 +0100 Subject: [PATCH 05/10] feat: routes and views for endpoint --- zubhub_backend/compose/web/dev/Dockerfile | 2 ++ zubhub_backend/compose/web/requirements.txt | 3 +- zubhub_backend/zubhub/activities/views.py | 38 ++++++++++++++------- 3 files changed, 29 insertions(+), 14 deletions(-) diff --git a/zubhub_backend/compose/web/dev/Dockerfile b/zubhub_backend/compose/web/dev/Dockerfile index ebff96967..c0db83312 100644 --- a/zubhub_backend/compose/web/dev/Dockerfile +++ b/zubhub_backend/compose/web/dev/Dockerfile @@ -11,6 +11,8 @@ RUN apt-get update \ && apt-get install -y libpq-dev \ # Translations dependencies && apt-get install -y gettext \ + # dependencies of Weasyprint + && apt install python3-pip python3-cffi python3-brotli libpango-1.0-0 libharfbuzz0b libpangoft2-1.0-0 -y\ # cleaning up unused files && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \ && rm -rf /var/lib/apt/lists/* diff --git a/zubhub_backend/compose/web/requirements.txt b/zubhub_backend/compose/web/requirements.txt index 67f934862..ce0341f64 100644 --- a/zubhub_backend/compose/web/requirements.txt +++ b/zubhub_backend/compose/web/requirements.txt @@ -71,6 +71,7 @@ python-dateutil>=2.8.1 python3-openid>=3.2.0 pytz>=2020.4 PyYAML>=5.4 +qrcode==7.4.2 requests>=2.23.0 requests-oauthlib>=1.3.0 s3transfer>=0.3.3 @@ -85,6 +86,6 @@ uritemplate>=3.0.1 urllib3>=1.25.11 vine>=1.3.0 watchdog>=0.10.2 -weasyprint>=60.2 +weasyprint==52.4 wcwidth>=0.2.5 whitenoise>=4.1.4 diff --git a/zubhub_backend/zubhub/activities/views.py b/zubhub_backend/zubhub/activities/views.py index 30a18efb3..a8191e14c 100644 --- a/zubhub_backend/zubhub/activities/views.py +++ b/zubhub_backend/zubhub/activities/views.py @@ -1,16 +1,15 @@ -from django.http import HttpResponse -from django.template.loader import render_to_string -from weasyprint import HTML from rest_framework.generics import ( ListAPIView, CreateAPIView, RetrieveAPIView, UpdateAPIView, DestroyAPIView) from rest_framework.permissions import IsAuthenticated, IsAuthenticatedOrReadOnly, AllowAny from .permissions import IsStaffOrModeratorOrEducator, IsOwner, IsStaffOrModerator from django.shortcuts import get_object_or_404 +from django.contrib.sites.shortcuts import get_current_site from .models import * from .serializers import * from django.db import transaction -from django.core.exceptions import PermissionDenied from django.contrib.auth.models import AnonymousUser +from .utils import generate_pdf, generate_qr_code +from django.conf import settings @@ -193,6 +192,7 @@ class DownloadActivityPDF(RetrieveAPIView): queryset = Activity.objects.all() permission_classes = [IsAuthenticated] serializer_class = ActivitySerializer + template_path = 'activities/activity_download.html' def get_object(self): pk = self.kwargs.get("pk") @@ -201,15 +201,27 @@ def get_object(self): def get(self, request, *args, **kwargs): activity = self.get_object() - + activity_images = ActivityImage.objects.filter(activity=activity) + activity_steps = ActivityMakingStep.objects.filter(activity=activity) + if settings.ENVIRONMENT == 'production': + qr_code = generate_qr_code( + link=f"https://zubhub.unstructured.studio/activities/{activity.id}" + ) + else: + qr_code = generate_qr_code( + link=f"{settings.DEFAULT_BACKEND_PROTOCOL}//{settings.DEFAULT_BACKEND_DOMAIN}/activities/{activity.id}" + ) context = { 'activity': activity, + 'activity_id': activity.id, + 'activity_images': activity_images, + 'activity_steps': activity_steps, + 'activity_steps_images': [step.image.all() for step in activity_steps], + 'activity_category': [category.name for category in activity.category.all()], + 'creators': [creator for creator in activity.creators.all()], + 'qr_code': qr_code } - html_string = render_to_string('activities/activity_download.html', context) - - html = HTML(string=html_string) - pdf = html.write_pdf() - - response = HttpResponse(pdf, content_type='application/pdf') - response['Content-Disposition'] = f'filename="{activity.id}.pdf"' - return response + return generate_pdf( + template_path=self.template_path, + context=context + ) From 139b4851108d69667a84fe37b9199947e297731f Mon Sep 17 00:00:00 2001 From: Steve Yonkeu Date: Fri, 19 Jan 2024 12:25:48 +0100 Subject: [PATCH 06/10] feat: routes and views for endpoint --- zubhub_backend/zubhub/activities/views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/zubhub_backend/zubhub/activities/views.py b/zubhub_backend/zubhub/activities/views.py index a8191e14c..dd007ecd0 100644 --- a/zubhub_backend/zubhub/activities/views.py +++ b/zubhub_backend/zubhub/activities/views.py @@ -191,6 +191,7 @@ class DownloadActivityPDF(RetrieveAPIView): """ queryset = Activity.objects.all() permission_classes = [IsAuthenticated] + # TODO: Add serializer a binary file serializer serializer_class = ActivitySerializer template_path = 'activities/activity_download.html' From b9925fb047d68f7a28463e0bdd08b6f955bd2da7 Mon Sep 17 00:00:00 2001 From: Steve Yonkeu Date: Wed, 24 Jan 2024 13:56:05 +0100 Subject: [PATCH 07/10] feat: utils for downloadable binaries --- zubhub_backend/zubhub/activities/models.py | 2 +- zubhub_backend/zubhub/activities/utils.py | 26 ++++++++++++++++--- zubhub_backend/zubhub/activities/views.py | 4 +-- .../activities/activity_download.html | 25 +++++++++++------- 4 files changed, 39 insertions(+), 18 deletions(-) diff --git a/zubhub_backend/zubhub/activities/models.py b/zubhub_backend/zubhub/activities/models.py index 6251fd589..75022b706 100644 --- a/zubhub_backend/zubhub/activities/models.py +++ b/zubhub_backend/zubhub/activities/models.py @@ -117,7 +117,7 @@ class ActivityImage(models.Model): blank=True) def __str__(self): - return self.image + return self.image.file_url class ActivityMakingStep(models.Model): diff --git a/zubhub_backend/zubhub/activities/utils.py b/zubhub_backend/zubhub/activities/utils.py index 5ac8b56ff..921366dd7 100644 --- a/zubhub_backend/zubhub/activities/utils.py +++ b/zubhub_backend/zubhub/activities/utils.py @@ -1,6 +1,7 @@ import io import base64 import qrcode +import requests from django.http import HttpResponse from django.template.loader import get_template from weasyprint import HTML @@ -40,21 +41,20 @@ def create_inspiring_examples(activity, inspiring_examples): def create_activity_images(activity, images): - for image in images: saved_image = Image.objects.create(**image['image']) ActivityImage.objects.create(activity=activity, image=saved_image) def update_image(image, image_data): - if(image_data is not None and image is not None): + if (image_data is not None and image is not None): if image_data["file_url"] == image.file_url: return image else: image.delete() return Image.objects.create(**image_data) else: - if(image): + if (image): image.delete() else: return Image.objects.create(**image_data) @@ -123,7 +123,7 @@ def generate_pdf(template_path, context): html = template.render(context) pdf = HTML(string=html).write_pdf() - + activity_id = context['activity_id'] response = HttpResponse(pdf, content_type="application/pdf") @@ -131,3 +131,21 @@ def generate_pdf(template_path, context): return response + +def download_file(file_url): + """ + Download a file from a given URL and save it to the local filesystem. + + Args: + file_url (str): The URL of the file to download. + + Returns: + bytes: The file data. + """ + response = requests.get(file_url, stream=True) + response.raise_for_status() + file_data = b"" + for chunk in response.iter_content(chunk_size=4096): + if chunk: + file_data += chunk + return file_data diff --git a/zubhub_backend/zubhub/activities/views.py b/zubhub_backend/zubhub/activities/views.py index dd007ecd0..6f75f871e 100644 --- a/zubhub_backend/zubhub/activities/views.py +++ b/zubhub_backend/zubhub/activities/views.py @@ -3,16 +3,14 @@ from rest_framework.permissions import IsAuthenticated, IsAuthenticatedOrReadOnly, AllowAny from .permissions import IsStaffOrModeratorOrEducator, IsOwner, IsStaffOrModerator from django.shortcuts import get_object_or_404 -from django.contrib.sites.shortcuts import get_current_site from .models import * from .serializers import * from django.db import transaction from django.contrib.auth.models import AnonymousUser -from .utils import generate_pdf, generate_qr_code +from .utils import generate_pdf, generate_qr_code, download_file from django.conf import settings - class ActivityListAPIView(ListAPIView): serializer_class = ActivitySerializer diff --git a/zubhub_backend/zubhub/templates/activities/activity_download.html b/zubhub_backend/zubhub/templates/activities/activity_download.html index 71d002344..bbc485c7f 100644 --- a/zubhub_backend/zubhub/templates/activities/activity_download.html +++ b/zubhub_backend/zubhub/templates/activities/activity_download.html @@ -218,12 +218,14 @@ {{ activity.introduction | safe }}
+ {% for activity_image in activity_images %} - - + + + {% endfor %}
@@ -252,10 +254,14 @@
- + />--> +
@@ -271,9 +277,8 @@
{% for img in activity_steps_images %} - {{ img.file_url }} - Activity image + Activity image {% endfor %}
From efa1b8494422cd88202a619a6213123bf3de60b5 Mon Sep 17 00:00:00 2001 From: Steve Yonkeu Date: Tue, 5 Mar 2024 10:07:33 +0100 Subject: [PATCH 08/10] feat: updates to pdf template --- zubhub_backend/zubhub/activities/views.py | 35 +- .../activities/activity_download.html | 530 +++++++++--------- 2 files changed, 284 insertions(+), 281 deletions(-) diff --git a/zubhub_backend/zubhub/activities/views.py b/zubhub_backend/zubhub/activities/views.py index 6f75f871e..b650d57f0 100644 --- a/zubhub_backend/zubhub/activities/views.py +++ b/zubhub_backend/zubhub/activities/views.py @@ -1,6 +1,8 @@ from rest_framework.generics import ( ListAPIView, CreateAPIView, RetrieveAPIView, UpdateAPIView, DestroyAPIView) from rest_framework.permissions import IsAuthenticated, IsAuthenticatedOrReadOnly, AllowAny +from rest_framework.response import Response +from rest_framework.views import APIView from .permissions import IsStaffOrModeratorOrEducator, IsOwner, IsStaffOrModerator from django.shortcuts import get_object_or_404 from .models import * @@ -19,7 +21,7 @@ class ActivityListAPIView(ListAPIView): def get_queryset(self): all = Activity.objects.all() return all - + class UserActivitiesAPIView(ListAPIView): """ @@ -29,7 +31,7 @@ class UserActivitiesAPIView(ListAPIView): serializer_class = ActivitySerializer permission_classes = [IsAuthenticated, IsOwner] - + def get_queryset(self): return self.request.user.activities_created.all() @@ -49,7 +51,7 @@ def get_object(self): queryset = self.get_queryset() pk = self.kwargs.get("pk") obj = get_object_or_404(queryset, pk=pk) - + if obj: with transaction.atomic(): if isinstance(self.request.user, AnonymousUser): @@ -61,7 +63,7 @@ def get_object(self): obj.views_count += 1 obj.save() return obj - + else: raise Exception() @@ -73,7 +75,7 @@ class PublishedActivitiesAPIView(ListAPIView): serializer_class = ActivitySerializer permission_classes = [AllowAny] - + def get_queryset(self): limit = self.request.query_params.get('limit', 10000) @@ -83,7 +85,7 @@ def get_queryset(self): limit = 10 return Activity.objects.filter(publish= True)[:limit] - + class UnPublishedActivitiesAPIView(ListAPIView): """ Fetch list of unpublished activities by authenticated staff member. @@ -96,7 +98,7 @@ class UnPublishedActivitiesAPIView(ListAPIView): permission_classes = [IsAuthenticated, IsStaffOrModerator] def get_queryset(self): - return Activity.objects.filter(publish= False) + return Activity.objects.filter(publish= False) class ActivityCreateAPIView(CreateAPIView): """ @@ -146,11 +148,11 @@ class ToggleSaveAPIView(RetrieveAPIView): queryset = Activity.objects.all() serializer_class = ActivitySerializer permission_classes = [IsAuthenticated] - + def get_object(self): pk = self.kwargs.get("pk") obj = get_object_or_404(self.get_queryset(), pk=pk) - + if self.request.user in obj.saved_by.all(): obj.saved_by.remove(self.request.user) obj.save() @@ -169,30 +171,31 @@ class togglePublishActivityAPIView(RetrieveAPIView): queryset = Activity.objects.all() serializer_class = ActivitySerializer permission_classes = [IsAuthenticated, IsStaffOrModerator] - + def get_object(self): - + pk = self.kwargs.get("pk") - obj = get_object_or_404(self.get_queryset(), pk=pk) + obj = get_object_or_404(self.get_queryset(), pk=pk) obj.publish = not obj.publish obj.save() return obj -class DownloadActivityPDF(RetrieveAPIView): +class DownloadActivityPDF(APIView): """ Download an activities. Requires activities id. Returns activities file. """ queryset = Activity.objects.all() - permission_classes = [IsAuthenticated] - # TODO: Add serializer a binary file serializer - serializer_class = ActivitySerializer template_path = 'activities/activity_download.html' + + def get_queryset(self): + return self.queryset + def get_object(self): pk = self.kwargs.get("pk") obj = get_object_or_404(self.get_queryset(), pk=pk) diff --git a/zubhub_backend/zubhub/templates/activities/activity_download.html b/zubhub_backend/zubhub/templates/activities/activity_download.html index bbc485c7f..f1313779c 100644 --- a/zubhub_backend/zubhub/templates/activities/activity_download.html +++ b/zubhub_backend/zubhub/templates/activities/activity_download.html @@ -1,322 +1,322 @@ - - + .footer .qr-code p { + font-size: 8px; + font-family: "Roboto", sans-serif; + color: #ffffff; + font-weight: 400; + margin: 0 25px 15px 10px; + white-space: nowrap; + } +
-
-
- {{ activity.title }} -
-
- {% if creators %} - {% for creator in creators %} - {{ creator.username }} avatar - {{ creator.username }} - {% endfor %} - {% endif %} -
+
+
+ {{ activity.title }}
-
-

- Introduction -

-
- {{ activity.introduction | safe }} -
-
- {% for activity_image in activity_images %} - +
+ {% if creators %} + {% for creator in creators %} + {{ creator.username }} avatar + {{ creator.username }} + {% endfor %} + {% endif %} +
+
+
+

+ Introduction +

+
+ {{ activity.introduction | safe }} +
+
+ {% for activity_image in activity_images %} + - {% endfor %} -
+ {% endfor %}
-
-

- Category -

-
- {% for category in activity_category %} - {{ category }} - {% endfor %} -
+
+
+

+ Category +

+
+ {% for category in activity_category %} + {{ category }} + {% endfor %}
-
-

- Class Grade -

-
- {{ activity.class_grade }} -
+
+
+

+ Class Grade +

+
+ {{ activity.class_grade }}
- {% if activity.materials_used %} -
-

Material Used

-
- {{ activity.materials_used | safe }} -
-
+
+ {% if activity.materials_used %} +
+

Material Used

+
+ {{ activity.materials_used | safe }} +
+
- - + + {{ activity.materials_used_image.file_url|convert_link }} + {{ activity.materials_used_image.file_url }} -
+
+
+ {% endif %} + {% if activity_steps %} + {% for step in activity_steps %} +
+

+ Step {{ step.step_order }}: {{ step.title }} +

+
+ {{ step.description | safe }}
- {% endif %} - {% if activity_steps %} - {% for step in activity_steps %} -
-

- Step {{ step.step_order }}: {{ step.title }} -

-
- {{ step.description | safe }} -
-
- {% for img in activity_steps_images %} - - Activity image +
+ {% for img in activity_steps_images %} + + Activity image - {% endfor %} -
-
- {% endfor %} - {% endif %} + {{ img.file_url|convert_link }} + {{ img.file_url }} + {% endfor %} +
+
+ {% endfor %} + {% endif %}
@@ -278,8 +276,6 @@ Activity image - {{ img.file_url|convert_link }} - {{ img.file_url }} {% endfor %}
From 083a902ef436cf39cecde3e0559639c02562ed17 Mon Sep 17 00:00:00 2001 From: Steve Yonkeu Date: Tue, 5 Mar 2024 11:37:03 +0100 Subject: [PATCH 10/10] feat: updates --- .../activities/activity_download.html | 4 +- zubhub_frontend/zubhub/src/api/api.js | 133 ++++++++++-------- .../activity_details/ActivityDetailsV2.jsx | 43 +++--- .../activityDetailsScripts.js | 18 ++- 4 files changed, 112 insertions(+), 86 deletions(-) diff --git a/zubhub_backend/zubhub/templates/activities/activity_download.html b/zubhub_backend/zubhub/templates/activities/activity_download.html index f95e639c6..cf6e5cac5 100644 --- a/zubhub_backend/zubhub/templates/activities/activity_download.html +++ b/zubhub_backend/zubhub/templates/activities/activity_download.html @@ -255,7 +255,7 @@
@@ -274,7 +274,7 @@
{% for img in activity_steps_images %} - Activity image + Activity image {% endfor %}
diff --git a/zubhub_frontend/zubhub/src/api/api.js b/zubhub_frontend/zubhub/src/api/api.js index 075b8f2dd..586d62c4c 100644 --- a/zubhub_frontend/zubhub/src/api/api.js +++ b/zubhub_frontend/zubhub/src/api/api.js @@ -27,12 +27,12 @@ class API { * @returns {Promise<>} */ request = ({ - url = '/', - method = 'GET', - token, - body, - content_type = 'application/json', - }) => { + url = '/', + method = 'GET', + token, + body, + content_type = 'application/json', + }) => { if (method === 'GET' && !token) { return fetch(this.domain + url, { method, @@ -139,16 +139,16 @@ class API { * @todo - describe method's signature */ signup = ({ - username, - email, - phone, - dateOfBirth, - user_location, - password1, - password2, - bio, - subscribe, - }) => { + username, + email, + phone, + dateOfBirth, + user_location, + password1, + password2, + bio, + subscribe, + }) => { const url = 'creators/register/'; const method = 'POST'; const body = JSON.stringify({ @@ -296,7 +296,7 @@ class API { const url = `creators/${groupname}/remove-member/${username}/`; const method = 'DELETE'; if (token) { - return this.request({ url, method ,token }).then(res => res.json()); + return this.request({ url, method, token }).then(res => res.json()); } else { return this.request({ url, method }).then(res => res.json()); } @@ -312,7 +312,7 @@ class API { const url = `creators/${groupname}/toggle-follow/${username}/`; const method = 'GET'; if (token) { - return this.request({ url, method ,token }).then(res => res.json()); + return this.request({ url, method, token }).then(res => res.json()); } else { return this.request({ url, method }).then(res => res.json()); } @@ -328,7 +328,7 @@ class API { const url = `creators/${groupname}/members/`; const method = 'GET'; if (token) { - return this.request({ url, method ,token }).then(res => res.json()); + return this.request({ url, method, token }).then(res => res.json()); } else { return this.request({ url, method }).then(res => res.json()); } @@ -340,7 +340,7 @@ class API { * * @todo - describe method's signature */ - teamMembersId = ( id ) => { + teamMembersId = (id) => { const url = `creators/id/${id}/`; const method = 'GET'; return this.request({ url, method }).then(res => res.json()); @@ -356,7 +356,7 @@ class API { const url = `creators/${groupname}/delete-group/`; const method = 'DELETE'; if (token) { - return this.request({ url, method ,token }).then(res => res.json()); + return this.request({ url, method, token }).then(res => res.json()); } else { return this.request({ url, method }).then(res => res.json()); } @@ -372,7 +372,7 @@ class API { const url = `creators/${groupname}/group-followers/`; const method = 'GET'; if (token) { - return this.request({ url, method ,token }).then(res => res.json()); + return this.request({ url, method, token }).then(res => res.json()); } else { return this.request({ url, method }).then(res => res.json()); } @@ -388,7 +388,7 @@ class API { const url = `creators/groups/${username}/`; const method = 'GET'; if (token) { - return this.request({ url, method ,token }).then(res => res.json()); + return this.request({ url, method, token }).then(res => res.json()); } else { return this.request({ url, method }).then(res => res.json()); } @@ -404,7 +404,7 @@ class API { const url = `creators/teams/`; const method = 'GET'; if (token) { - return this.request({ url, method ,token }).then(res => res.json()); + return this.request({ url, method, token }).then(res => res.json()); } else { return this.request({ url, method }).then(res => res.json()); } @@ -422,7 +422,7 @@ class API { const content_type = false; const body = data; if (token) { - return this.request({ url, method ,token, body, content_type }).then(res => res.json()); + return this.request({ url, method, token, body, content_type }).then(res => res.json()); } else { return this.request({ url, method }).then(res => res.json()); } @@ -439,7 +439,7 @@ class API { const method = 'POST'; const content_type = 'application/json'; const body = JSON.stringify(data); - + if (token) { return this.request({ url, method, token, body, content_type }).then(res => res.json()); } else { @@ -459,7 +459,7 @@ class API { const content_type = 'application/json'; const body = JSON.stringify(data); if (token) { - return this.request({ url, method ,token, body, content_type }).then(res => res.json()); + return this.request({ url, method, token, body, content_type }).then(res => res.json()); } else { return this.request({ url, method }).then(res => res.json()); } @@ -732,17 +732,17 @@ class API { * @todo - describe method's signature */ createProject = ({ - token, - title, - description, - video, - images, - materials_used, - category, - tags, - publish, - activity, - }) => { + token, + title, + description, + video, + images, + materials_used, + category, + tags, + publish, + activity, + }) => { const url = 'projects/create/'; const method = 'POST'; const body = JSON.stringify({ @@ -766,17 +766,17 @@ class API { * @todo - describe method's signature */ updateProject = ({ - token, - id, - title, - description, - video, - images, - materials_used, - category, - tags, - publish, - }) => { + token, + id, + title, + description, + video, + images, + materials_used, + category, + tags, + publish, + }) => { const url = `projects/${id}/update/`; const method = 'PATCH'; @@ -862,11 +862,11 @@ class API { }; /** - * @method getActivity - * @author Yaya Mamoudou - * - * @todo - describe method's signature - */ + * @method getActivity + * @author Yaya Mamoudou + * + * @todo - describe method's signature + */ getActivity = ({ token, id }) => { const url = `activities/${id}`; return this.request({ token, url }).then(res => res.json()); @@ -1024,10 +1024,10 @@ class API { }; /** - * @method getChallenge - * @author Suchakra Sharma - * - */ + * @method getChallenge + * @author Suchakra Sharma + * + */ getChallenge = () => { const url = `challenge/`; @@ -1166,6 +1166,23 @@ class API { const url = `activities/${id}/toggle-publish/`; return this.request({ url, token }).then(res => res.json()); }; + + activityDownload = async ({ id, token }) => { + const url = `${this.domain}activities/${id}/pdf/`; + const response = await fetch(url, { + method: 'GET', + headers: { + 'Authorization': `Token ${token}` + } + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } else { + const blob = await response.blob(); + return blob; + } + }; } export default API; diff --git a/zubhub_frontend/zubhub/src/views/activity_details/ActivityDetailsV2.jsx b/zubhub_frontend/zubhub/src/views/activity_details/ActivityDetailsV2.jsx index adad29421..09e8fbaf1 100644 --- a/zubhub_frontend/zubhub/src/views/activity_details/ActivityDetailsV2.jsx +++ b/zubhub_frontend/zubhub/src/views/activity_details/ActivityDetailsV2.jsx @@ -30,8 +30,6 @@ import Activity from '../../components/activity/activity'; import SocialButtons from '../../components/social_share_buttons/socialShareButtons'; import { getUrlQueryObject } from '../../utils.js'; import { activityDefailsStyles } from './ActivityDetails.styles'; -import { useReactToPrint } from 'react-to-print'; -import Html2Pdf from 'html2pdf.js'; const API = new ZubHubAPI(); const authenticatedUserActivitiesGrid = { xs: 12, sm: 6, md: 6 }; @@ -86,29 +84,24 @@ export default function ActivityDetailsV2(props) { props.navigate(window.location.pathname, { replace: true }); }; - const handleDownload = useReactToPrint({ - onBeforePrint: () => setIsDownloading(true), - onPrintError: error => setIsDownloading(false), - onAfterPrint: () => setIsDownloading(false), - content: () => ref.current, - removeAfterPrint: true, - print: async printIframe => { - const document = printIframe.contentDocument; - if (document) { - const html = document.getElementsByTagName('html')[0]; - const pdfOptions = { - padding: 10, - filename: `${activity.title}.pdf`, - image: { type: 'jpeg', quality: 0.98 }, - html2canvas: { scale: 2, useCORS: true }, - jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }, - }; - - const exporter = new Html2Pdf(html, pdfOptions); - await exporter.getPdf(true); - } - }, - }); + const handleDownload = () => { + setIsDownloading(true); + API.activityDownload({ token: auth.token, id: activity.id }) + .then(res => { + // Assuming 'res' is the blob itself, no need for 'res.blob()' + const url = window.URL.createObjectURL(new Blob([res])); + const link = document.createElement('a'); + link.href = url; + link.setAttribute('download', `${activity.title}.pdf`); + document.body.appendChild(link); + link.click(); + link.remove(); + }) + .catch(error => { + console.error('Error downloading PDF:', error); + }) + .finally(() => setIsDownloading(false)); + }; return (
diff --git a/zubhub_frontend/zubhub/src/views/activity_details/activityDetailsScripts.js b/zubhub_frontend/zubhub/src/views/activity_details/activityDetailsScripts.js index 5b74c5a09..726294996 100644 --- a/zubhub_frontend/zubhub/src/views/activity_details/activityDetailsScripts.js +++ b/zubhub_frontend/zubhub/src/views/activity_details/activityDetailsScripts.js @@ -7,7 +7,7 @@ const API = new ZubhubAPI(); export const deleteActivity = args => { return API.deleteActivity({ token: args.token, id: args.id }).then(res => { if (res.status === 204) { - toast.success(args.t('activityDetails.activity.delete.dialog.success')); + toast.success(args.t('activityDetails.activity.delete.dialog.forbidden')); return args.navigate('/activities'); } else { if (res.status === 403 && res.statusText === 'Forbidden') { @@ -21,6 +21,22 @@ export const deleteActivity = args => { }); }; +export const activityDownload = async args => { + const response = await API.activityDownload({ token: args.token, id: args.id }); + + if (response.status === 200) { + const blob = await response.blob(); + return blob; + } else { + if (response.status === 403 && response.statusText === 'Forbidden') { + toast.warning(args.t('activityDetails.activity.download.dialog.forbidden')); + } else { + toast.warning(args.t('activities.errors.dialog.serverError')); + } + } +} + + export const togglePublish = async ( e, id,