1515"""
1616contains all the views related to Mechanic
1717"""
18+ import os
1819import bcrypt
20+ import re
21+ from urllib .parse import unquote
22+ from django .template .loader import get_template
23+ from xhtml2pdf import pisa
1924from django .utils import timezone
2025from django .views .decorators .csrf import csrf_exempt
2126from django .urls import reverse
2227from rest_framework import status
2328from rest_framework .response import Response
2429from rest_framework .views import APIView
2530from django .db import models
31+ from django .http import FileResponse
2632from crapi_site import settings
2733from utils .jwt import jwt_auth_required
2834from utils import messages
4046)
4147from rest_framework .pagination import LimitOffsetPagination
4248
43-
4449class SignUpView (APIView ):
4550 """
4651 Used to add a new mechanic
@@ -235,6 +240,7 @@ def get(self, request, user=None):
235240 )
236241 serializer = MechanicServiceRequestSerializer (service_request )
237242 response_data = dict (serializer .data )
243+ service_report_pdf (response_data , report_id )
238244 return Response (response_data , status = status .HTTP_200_OK )
239245
240246
@@ -366,3 +372,89 @@ def get(self, request, user=None, service_request_id=None):
366372 service_request = ServiceRequest .objects .get (id = service_request_id )
367373 serializer = MechanicServiceRequestSerializer (service_request )
368374 return Response (serializer .data , status = status .HTTP_200_OK )
375+
376+
377+ class DownloadReportView (APIView ):
378+ """
379+ A view to download a service report.
380+ """
381+ def get (self , request , format = None ):
382+ filename_from_user = request .query_params .get ('filename' )
383+ if not filename_from_user :
384+ return Response (
385+ {"message" : "Parameter 'filename' is required." },
386+ status = status .HTTP_400_BAD_REQUEST
387+ )
388+ #Checks if input before decoding contains only allowed characters
389+ if not validate_filename (filename_from_user ):
390+ return Response (
391+ {"message" : "Invalid input." },
392+ status = status .HTTP_400_BAD_REQUEST
393+ )
394+
395+ filename_from_user = unquote (filename_from_user )
396+ full_path = os .path .abspath (os .path .join (settings .BASE_DIR , "reports" , filename_from_user ))
397+ if os .path .exists (full_path ) and os .path .isfile (full_path ):
398+ return FileResponse (open (full_path , 'rb' ))
399+ elif not os .path .exists (full_path ):
400+ return Response (
401+ {"message" : f"File not found at '{ full_path } '." },
402+ status = status .HTTP_404_NOT_FOUND
403+ )
404+ else :
405+ return Response (
406+ {"message" : f"'{ full_path } ' is not a file." },
407+ status = status .HTTP_403_FORBIDDEN
408+ )
409+
410+ def validate_filename (input : str ) -> bool :
411+ """
412+ Allowed: alphanumerics, _, :, %HH
413+ """
414+ url_encoded_pattern = re .compile (r'^(?:[A-Za-z0-9:_]|%[0-9A-Fa-f]{2})*$' )
415+ return bool (url_encoded_pattern .fullmatch (input ))
416+
417+
418+ def service_report_pdf (response_data , report_id ):
419+ """
420+ Generates service report's PDF file from a template and saves it to the disk.
421+ """
422+ reports_dir = os .path .join (settings .BASE_DIR , 'reports' )
423+ os .makedirs (reports_dir , exist_ok = True )
424+ report_filepath = os .path .join (reports_dir , f"report_{ report_id } " )
425+
426+ template = get_template ('service_report.html' )
427+ html_string = template .render ({'service' : response_data })
428+ with open (report_filepath , "w+b" ) as pdf_file :
429+ pisa .CreatePDF (src = html_string , dest = pdf_file )
430+
431+ manage_reports_directory ()
432+
433+
434+ def manage_reports_directory ():
435+ """
436+ Checks reports directory and deletes the oldest one if the
437+ count exceeds the maximum limit.
438+ """
439+ try :
440+ reports_dir = os .path .join (settings .BASE_DIR , 'reports' )
441+ report_files = os .listdir (reports_dir )
442+
443+ if len (report_files ) >= settings .FILES_LIMIT :
444+ oldest_file = None
445+ oldest_time = float ('inf' )
446+ for filename in report_files :
447+ filepath = os .path .join (reports_dir , filename )
448+ try :
449+ current_mtime = os .path .getmtime (filepath )
450+ if current_mtime < oldest_time :
451+ oldest_time = current_mtime
452+ oldest_file = filepath
453+ except FileNotFoundError :
454+ continue
455+
456+ if oldest_file :
457+ os .remove (oldest_file )
458+
459+ except (OSError , FileNotFoundError ) as e :
460+ print (f"Error during report directory management: { e } " )
0 commit comments