Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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=1000
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: 1000
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": "Invalid 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", 1000))

# 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