Skip to content

Commit 74c29ab

Browse files
yokwejusteNdibe Raymond Olisaemeka
authored and
Ndibe Raymond Olisaemeka
committed
translates download button
add utils for qr code and pdf rendering implements template for pdf routes and views for endpoint utils for downloadable binaries updates to pdf template
1 parent c3f4f34 commit 74c29ab

File tree

12 files changed

+538
-69
lines changed

12 files changed

+538
-69
lines changed

Diff for: zubhub_backend/compose/celery/requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ uritemplate>=3.0.1
8282
urllib3>=1.25.11
8383
vine>=1.3.0
8484
watchdog>=0.10.2
85+
weasyprint>=60.2
8586
wcwidth>=0.2.5
8687
whitenoise>=4.1.4
8788
django-extensions>=1.0.0

Diff for: zubhub_backend/compose/web/dev/Dockerfile

+2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ RUN apt-get update \
1111
&& apt-get install -y libpq-dev \
1212
# Translations dependencies
1313
&& apt-get install -y gettext \
14+
# dependencies of Weasyprint
15+
&& apt install python3-pip python3-cffi python3-brotli libpango-1.0-0 libharfbuzz0b libpangoft2-1.0-0 -y\
1416
# cleaning up unused files
1517
&& apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \
1618
&& rm -rf /var/lib/apt/lists/*

Diff for: zubhub_backend/compose/web/requirements.txt

+2
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ python-dateutil>=2.8.1
7171
python3-openid>=3.2.0
7272
pytz>=2020.4
7373
PyYAML>=5.4
74+
qrcode==7.4.2
7475
requests>=2.23.0
7576
requests-oauthlib>=1.3.0
7677
s3transfer>=0.3.3
@@ -85,5 +86,6 @@ uritemplate>=3.0.1
8586
urllib3>=1.25.11
8687
vine>=1.3.0
8788
watchdog>=0.10.2
89+
weasyprint==52.4
8890
wcwidth>=0.2.5
8991
whitenoise>=4.1.4

Diff for: zubhub_backend/zubhub/activities/urls.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,6 @@
1212
path('<uuid:pk>/delete/', ActivityDeleteAPIView.as_view(), name='delete'),
1313
path('<uuid:pk>/toggle-save/', ToggleSaveAPIView.as_view(), name='save'),
1414
path('<uuid:pk>/toggle-publish/', togglePublishActivityAPIView.as_view(), name='publish'),
15-
path('<uuid:pk>/', ActivityDetailsAPIView.as_view(), name='detail_activity')
15+
path('<uuid:pk>/', ActivityDetailsAPIView.as_view(), name='detail_activity'),
16+
path('<uuid:pk>/pdf/', DownloadActivityPDF.as_view(), name='pdf'),
1617
]

Diff for: zubhub_backend/zubhub/activities/utils.py

+85-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
import io
2+
import base64
3+
import qrcode
4+
import requests
5+
from django.http import HttpResponse
6+
from django.template.loader import get_template
7+
from weasyprint import HTML
18
from .models import *
29

310

@@ -34,21 +41,20 @@ def create_inspiring_examples(activity, inspiring_examples):
3441

3542

3643
def create_activity_images(activity, images):
37-
3844
for image in images:
3945
saved_image = Image.objects.create(**image['image'])
4046
ActivityImage.objects.create(activity=activity, image=saved_image)
4147

4248

4349
def update_image(image, image_data):
44-
if(image_data is not None and image is not None):
50+
if (image_data is not None and image is not None):
4551
if image_data["file_url"] == image.file_url:
4652
return image
4753
else:
4854
image.delete()
4955
return Image.objects.create(**image_data)
5056
else:
51-
if(image):
57+
if (image):
5258
image.delete()
5359
else:
5460
return Image.objects.create(**image_data)
@@ -67,3 +73,79 @@ def update_making_steps(activity, making_steps):
6773
def update_inspiring_examples(activity, inspiring_examples):
6874
InspiringExample.objects.filter(activity=activity).delete()
6975
create_inspiring_examples(activity, inspiring_examples)
76+
77+
78+
def generate_qr_code(link):
79+
"""
80+
Generate a QR code for a given link and return it as a base64 string.
81+
82+
Args:
83+
link (str): The link to encode in the QR code.
84+
85+
Returns:
86+
str: The QR code as a base64 string.
87+
"""
88+
# Generate QR code
89+
qr = qrcode.QRCode(
90+
version=1,
91+
error_correction=qrcode.constants.ERROR_CORRECT_L,
92+
box_size=10,
93+
border=4,
94+
)
95+
qr.add_data(link)
96+
qr.make(fit=True)
97+
98+
img = qr.make_image(fill_color="black", back_color="white")
99+
100+
buf = io.BytesIO()
101+
img.save(buf, format="PNG")
102+
103+
img_bytes = buf.getvalue()
104+
105+
img_base64 = base64.b64encode(img_bytes).decode()
106+
107+
return img_base64
108+
109+
110+
def generate_pdf(template_path, context):
111+
"""
112+
Generate a PDF file from a Jinja template.
113+
114+
Args:
115+
template_path (str): The file path to the Jinja template.
116+
context (dict): The context data for rendering the template.
117+
118+
Returns:
119+
HttpResponse: A Django HTTP response with the generated PDF.
120+
"""
121+
template = get_template(template_path)
122+
123+
html = template.render(context)
124+
125+
pdf = HTML(string=html).write_pdf()
126+
127+
activity_id = context['activity_id']
128+
129+
response = HttpResponse(pdf, content_type="application/pdf")
130+
response["Content-Disposition"] = f'attachment; filename="{activity_id}.pdf"'
131+
132+
return response
133+
134+
135+
def download_file(file_url):
136+
"""
137+
Download a file from a given URL and save it to the local filesystem.
138+
139+
Args:
140+
file_url (str): The URL of the file to download.
141+
142+
Returns:
143+
bytes: The file data.
144+
"""
145+
response = requests.get(file_url, stream=True)
146+
response.raise_for_status()
147+
file_data = b""
148+
for chunk in response.iter_content(chunk_size=4096):
149+
if chunk:
150+
file_data += chunk
151+
return file_data

Diff for: zubhub_backend/zubhub/activities/views.py

+63-18
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
1-
from django.shortcuts import render
2-
from django.utils.translation import ugettext_lazy as _
3-
from rest_framework.decorators import api_view
4-
from rest_framework.response import Response
51
from rest_framework.generics import (
62
ListAPIView, CreateAPIView, RetrieveAPIView, UpdateAPIView, DestroyAPIView)
73
from rest_framework.permissions import IsAuthenticated, IsAuthenticatedOrReadOnly, AllowAny
4+
from rest_framework.response import Response
5+
from rest_framework.views import APIView
86
from .permissions import IsStaffOrModeratorOrEducator, IsOwner, IsStaffOrModerator
97
from django.shortcuts import get_object_or_404
108
from .models import *
119
from .serializers import *
1210
from django.db import transaction
13-
from django.core.exceptions import PermissionDenied
1411
from django.contrib.auth.models import AnonymousUser
15-
12+
from .utils import generate_pdf, generate_qr_code, download_file
13+
from django.conf import settings
1614

1715

1816
class ActivityListAPIView(ListAPIView):
@@ -23,7 +21,7 @@ class ActivityListAPIView(ListAPIView):
2321
def get_queryset(self):
2422
all = Activity.objects.all()
2523
return all
26-
24+
2725

2826
class UserActivitiesAPIView(ListAPIView):
2927
"""
@@ -33,7 +31,7 @@ class UserActivitiesAPIView(ListAPIView):
3331

3432
serializer_class = ActivitySerializer
3533
permission_classes = [IsAuthenticated, IsOwner]
36-
34+
3735
def get_queryset(self):
3836
return self.request.user.activities_created.all()
3937

@@ -53,7 +51,7 @@ def get_object(self):
5351
queryset = self.get_queryset()
5452
pk = self.kwargs.get("pk")
5553
obj = get_object_or_404(queryset, pk=pk)
56-
54+
5755
if obj:
5856
with transaction.atomic():
5957
if isinstance(self.request.user, AnonymousUser):
@@ -65,7 +63,7 @@ def get_object(self):
6563
obj.views_count += 1
6664
obj.save()
6765
return obj
68-
66+
6967
else:
7068
raise Exception()
7169

@@ -77,7 +75,7 @@ class PublishedActivitiesAPIView(ListAPIView):
7775

7876
serializer_class = ActivitySerializer
7977
permission_classes = [AllowAny]
80-
78+
8179
def get_queryset(self):
8280
limit = self.request.query_params.get('limit', 10000)
8381

@@ -87,7 +85,7 @@ def get_queryset(self):
8785
limit = 10
8886

8987
return Activity.objects.filter(publish= True)[:limit]
90-
88+
9189
class UnPublishedActivitiesAPIView(ListAPIView):
9290
"""
9391
Fetch list of unpublished activities by authenticated staff member.
@@ -100,7 +98,7 @@ class UnPublishedActivitiesAPIView(ListAPIView):
10098
permission_classes = [IsAuthenticated, IsStaffOrModerator]
10199

102100
def get_queryset(self):
103-
return Activity.objects.filter(publish= False)
101+
return Activity.objects.filter(publish= False)
104102

105103
class ActivityCreateAPIView(CreateAPIView):
106104
"""
@@ -150,11 +148,11 @@ class ToggleSaveAPIView(RetrieveAPIView):
150148
queryset = Activity.objects.all()
151149
serializer_class = ActivitySerializer
152150
permission_classes = [IsAuthenticated]
153-
151+
154152
def get_object(self):
155153
pk = self.kwargs.get("pk")
156154
obj = get_object_or_404(self.get_queryset(), pk=pk)
157-
155+
158156
if self.request.user in obj.saved_by.all():
159157
obj.saved_by.remove(self.request.user)
160158
obj.save()
@@ -173,12 +171,59 @@ class togglePublishActivityAPIView(RetrieveAPIView):
173171
queryset = Activity.objects.all()
174172
serializer_class = ActivitySerializer
175173
permission_classes = [IsAuthenticated, IsStaffOrModerator]
176-
174+
177175

178176
def get_object(self):
179-
177+
180178
pk = self.kwargs.get("pk")
181-
obj = get_object_or_404(self.get_queryset(), pk=pk)
179+
obj = get_object_or_404(self.get_queryset(), pk=pk)
182180
obj.publish = not obj.publish
183181
obj.save()
184182
return obj
183+
184+
185+
186+
class DownloadActivityPDF(APIView):
187+
"""
188+
Download an activities.
189+
Requires activities id.
190+
Returns activities file.
191+
"""
192+
queryset = Activity.objects.all()
193+
template_path = 'activities/activity_download.html'
194+
195+
196+
def get_queryset(self):
197+
return self.queryset
198+
199+
def get_object(self):
200+
pk = self.kwargs.get("pk")
201+
obj = get_object_or_404(self.get_queryset(), pk=pk)
202+
return obj
203+
204+
def get(self, request, *args, **kwargs):
205+
activity = self.get_object()
206+
activity_images = ActivityImage.objects.filter(activity=activity)
207+
activity_steps = ActivityMakingStep.objects.filter(activity=activity)
208+
if settings.ENVIRONMENT == 'production':
209+
qr_code = generate_qr_code(
210+
link=f"https://zubhub.unstructured.studio/activities/{activity.id}"
211+
)
212+
else:
213+
qr_code = generate_qr_code(
214+
link=f"{settings.DEFAULT_BACKEND_PROTOCOL}//{settings.DEFAULT_BACKEND_DOMAIN}/activities/{activity.id}"
215+
)
216+
context = {
217+
'activity': activity,
218+
'activity_id': activity.id,
219+
'activity_images': activity_images,
220+
'activity_steps': activity_steps,
221+
'activity_steps_images': [step.image.all() for step in activity_steps],
222+
'activity_category': [category.name for category in activity.category.all()],
223+
'creators': [creator for creator in activity.creators.all()],
224+
'qr_code': qr_code
225+
}
226+
return generate_pdf(
227+
template_path=self.template_path,
228+
context=context
229+
)

0 commit comments

Comments
 (0)