Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions deploy/docker/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ services:
- TLS_ENABLED=${TLS_ENABLED:-false}
- TLS_CERTIFICATE=certs/server.crt
- TLS_KEY=certs/server.key
- FILES_LIMIT=50
depends_on:
postgresdb:
condition: service_healthy
Expand Down
1 change: 1 addition & 0 deletions deploy/helm/templates/workshop/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ data:
SERVER_PORT: {{ .Values.workshop.port | quote }}
API_GATEWAY_URL: {{ if .Values.apiGatewayServiceInstall }}"https://{{ .Values.apiGatewayService.service.name }}"{{ else }}{{ .Values.apiGatewayServiceUrl }}{{ end }}
TLS_ENABLED: {{ .Values.tlsEnabled | quote }}
FILES_LIMIT: {{ .Values.workshop.config.filesLimit }}
1 change: 1 addition & 0 deletions deploy/helm/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ workshop:
postgresDbDriver: postgres
mongoDbDriver: mongodb
secretKey: crapi
filesLimit: 50
deploymentLabels:
app: crapi-workshop
podLabels:
Expand Down
25 changes: 15 additions & 10 deletions services/web/src/components/serviceReport/serviceReport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,7 @@
*/

import React from "react";
import {
Card,
Row,
Col,
Descriptions,
Spin,
Layout,
Timeline,
Typography,
} from "antd";
import { Card, Descriptions, Spin, Layout, Timeline, Typography } from "antd";
import { PageHeader } from "@ant-design/pro-components";
import { Content } from "antd/es/layout/layout";
import {
Expand All @@ -33,6 +24,7 @@ import {
ToolOutlined,
CommentOutlined,
CalendarOutlined,
DownloadOutlined,
} from "@ant-design/icons";
import "./styles.css";

Expand Down Expand Up @@ -65,6 +57,7 @@ interface Service {
comment: string;
created_on: string;
}[];
downloadUrl?: string;
}

interface ServiceReportProps {
Expand Down Expand Up @@ -126,6 +119,18 @@ const ServiceReport: React.FC<ServiceReportProps> = ({ service }) => {
</Text>
</div>
}
extra={[
<a
key="1"
className="download-report-button"
href={service.downloadUrl}
target="_blank"
rel="noopener noreferrer"
>
<DownloadOutlined />
Download Report
</a>,
]}
/>
</div>

Expand Down
33 changes: 33 additions & 0 deletions services/web/src/components/serviceReport/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,34 @@
gap: var(--spacing-sm);
}

/* Download Report button */
.download-report-button {
display: inline-flex;
align-items: center;
gap: var(--spacing-xs);
background: linear-gradient(135deg, #8b5cf6 0%, #a855f7 100%);
color: white;
text-decoration: none;
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--border-radius-lg);
font-size: 14px;
font-weight: 600;
transition: all 0.3s ease;
border: none;
cursor: pointer;
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
width: 100%;
justify-content: center;
}

.download-report-button:hover, .download-report-button:focus {
background: linear-gradient(135deg, #7c3aed 0%, #9333ea 100%);
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(139, 92, 246, 0.4);
color: white;
text-decoration: none;
}

/* Loading State */
.loading-container {
display: flex;
Expand Down Expand Up @@ -251,6 +279,11 @@
padding: var(--spacing-md);
}

.download-report-button {
padding: var(--spacing-md);
font-size: 13px;
}

.ant-descriptions-item-label {
font-size: 12px;
}
Expand Down
1 change: 1 addition & 0 deletions services/web/src/constants/APIConstant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export const requestURLS: RequestURLSType = {
UPDATE_SERVICE_REQUEST_STATUS: "api/mechanic/service_request/<serviceId>",
GET_VEHICLE_SERVICES: "api/merchant/service_requests/<vehicleVIN>",
GET_SERVICE_REPORT: "api/mechanic/mechanic_report",
DOWNLOAD_SERVICE_REPORT: "api/mechanic/download_report",
BUY_PRODUCT: "api/shop/orders",
GET_ORDERS: "api/shop/orders/all",
GET_ORDER_BY_ID: "api/shop/orders/<orderId>",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ interface Service {
comment: string;
created_on: string;
}[];
downloadUrl?: string;
}

const mapStateToProps = (state: RootState) => ({
Expand Down
6 changes: 6 additions & 0 deletions services/web/src/sagas/vehicleSaga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,12 @@ export function* getServiceReport(action: MyAction): Generator<any, void, any> {
throw responseJSON;
}

const filename = `report_${reportId}`;
responseJSON.downloadUrl =
APIService.WORKSHOP_SERVICE +
requestURLS.DOWNLOAD_SERVICE_REPORT +
"?filename=" +
filename;
yield put({ type: actionTypes.FETCHED_DATA, payload: responseJSON });
callback(responseTypes.SUCCESS, responseJSON);
} catch (e) {
Expand Down
1 change: 1 addition & 0 deletions services/workshop/crapi/mechanic/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,6 @@
r"service_request$",
mechanic_views.MechanicServiceRequestsView.as_view(),
),
re_path(r"download_report$", mechanic_views.DownloadReportView.as_view()),
re_path(r"$", mechanic_views.MechanicView.as_view()),
]
94 changes: 93 additions & 1 deletion services/workshop/crapi/mechanic/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,20 @@
"""
contains all the views related to Mechanic
"""
import os
import bcrypt
import re
from urllib.parse import unquote
from django.template.loader import get_template
from xhtml2pdf import pisa
from django.utils import timezone
from django.views.decorators.csrf import csrf_exempt
from django.urls import reverse
from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView
from django.db import models
from django.http import FileResponse
from crapi_site import settings
from utils.jwt import jwt_auth_required
from utils import messages
Expand All @@ -40,7 +46,6 @@
)
from rest_framework.pagination import LimitOffsetPagination


class SignUpView(APIView):
"""
Used to add a new mechanic
Expand Down Expand Up @@ -235,6 +240,7 @@ def get(self, request, user=None):
)
serializer = MechanicServiceRequestSerializer(service_request)
response_data = dict(serializer.data)
service_report_pdf(response_data, report_id)
return Response(response_data, status=status.HTTP_200_OK)


Expand Down Expand Up @@ -366,3 +372,89 @@ def get(self, request, user=None, service_request_id=None):
service_request = ServiceRequest.objects.get(id=service_request_id)
serializer = MechanicServiceRequestSerializer(service_request)
return Response(serializer.data, status=status.HTTP_200_OK)


class DownloadReportView(APIView):
"""
A view to download a service report.
"""
def get(self, request, format=None):
filename_from_user = request.query_params.get('filename')
if not filename_from_user:
return Response(
{"message": "Parameter 'filename' is required."},
status=status.HTTP_400_BAD_REQUEST
)
#Checks if input before decoding contains only allowed characters
if not validate_filename(filename_from_user):
return Response(
{"message": "Forbidden input."},
status=status.HTTP_400_BAD_REQUEST
)

filename_from_user = unquote(filename_from_user)
full_path = os.path.abspath(os.path.join(settings.BASE_DIR, "reports", filename_from_user))
if os.path.exists(full_path) and os.path.isfile(full_path):
return FileResponse(open(full_path, 'rb'))
elif not os.path.exists(full_path):
return Response(
{"message": f"File not found at '{full_path}'."},
status=status.HTTP_404_NOT_FOUND
)
else:
return Response(
{"message": f"'{full_path}' is not a file."},
status=status.HTTP_403_FORBIDDEN
)

def validate_filename(input: str) -> bool:
"""
Allowed: alphanumerics, _, :, %HH
"""
url_encoded_pattern = re.compile(r'^(?:[A-Za-z0-9:_]|%[0-9A-Fa-f]{2})*$')
return bool(url_encoded_pattern.fullmatch(input))


def service_report_pdf(response_data, report_id):
"""
Generates service report's PDF file from a template and saves it to the disk.
"""
reports_dir = os.path.join(settings.BASE_DIR, 'reports')
os.makedirs(reports_dir, exist_ok=True)
report_filepath = os.path.join(reports_dir, f"report_{report_id}")

template = get_template('service_report.html')
html_string = template.render({'service': response_data})
with open(report_filepath, "w+b") as pdf_file:
pisa.CreatePDF(src=html_string, dest=pdf_file)

manage_reports_directory()


def manage_reports_directory():
"""
Checks reports directory and deletes the oldest one if the
count exceeds the maximum limit.
"""
try:
reports_dir = os.path.join(settings.BASE_DIR, 'reports')
report_files = os.listdir(reports_dir)

if len(report_files) >= settings.FILES_LIMIT:
oldest_file = None
oldest_time = float('inf')
for filename in report_files:
filepath = os.path.join(reports_dir, filename)
try:
current_mtime = os.path.getmtime(filepath)
if current_mtime < oldest_time:
oldest_time = current_mtime
oldest_file = filepath
except FileNotFoundError:
continue

if oldest_file:
os.remove(oldest_file)

except (OSError, FileNotFoundError) as e:
print(f"Error during report directory management: {e}")
4 changes: 3 additions & 1 deletion services/workshop/crapi_site/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ def get_env_value(env_variable):
raise ImproperlyConfigured(error_msg)


FILES_LIMIT = int(os.environ.get("FILES_LIMIT", 50))

# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

Expand Down Expand Up @@ -108,7 +110,7 @@ def get_env_value(env_variable):
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"DIRS": [os.path.join(BASE_DIR, 'utils')],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
Expand Down
1 change: 1 addition & 0 deletions services/workshop/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ gunicorn==21.2.0
coverage==7.4.1
unittest-xml-reporting==3.2.0
black==24.4.2
xhtml2pdf==0.2.17
Loading
Loading