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
8 changes: 0 additions & 8 deletions backend/apps/owasp/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,6 @@ owasp-enrich-projects:
@echo "Enriching OWASP projects"
@CMD="python manage.py owasp_enrich_projects" $(MAKE) exec-backend-command

owasp-generate-project-health-metrics-overview-pdf:
@echo "Generating OWASP project health metrics overview PDF"
@CMD="python manage.py owasp_generate_project_health_metrics_overview_pdf" $(MAKE) exec-backend-command

owasp-generate-project-health-metrics-pdf:
@echo "Generating OWASP project health metrics PDF for project: $(project_key)"
@CMD="python manage.py owasp_generate_project_health_metrics_pdf --project-key=$(project_key)" $(MAKE) exec-backend-command

owasp-process-snapshots:
@echo "Processing OWASP snapshots"
@CMD="python manage.py owasp_process_snapshots" $(MAKE) exec-backend-command
Expand Down
28 changes: 24 additions & 4 deletions backend/apps/owasp/api/internal/views/project_health_metrics.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,36 @@
"""Views for OWASP project health metrics."""

from django.http import FileResponse
from django.http import FileResponse, Http404
from django.views.decorators.http import require_GET

from apps.owasp.utils.pdf import generate_metrics_overview_pdf
from apps.owasp.models.project import Project
from apps.owasp.models.project_health_metrics import ProjectHealthMetrics
from apps.owasp.utils.pdf import generate_latest_metrics_pdf, generate_metrics_overview_pdf


@require_GET
def generate_overview_pdf(_request):
def generate_overview_pdf(request): # noqa: ARG001
"""Generate a PDF overview of OWASP project health metrics."""
return FileResponse(
generate_metrics_overview_pdf(),
generate_metrics_overview_pdf(ProjectHealthMetrics.get_stats()),
as_attachment=True,
filename="owasp_project_health_metrics_overview.pdf",
)


@require_GET
def generate_project_health_metrics_pdf(request, project_key: str): # noqa: ARG001
"""Generate and return a PDF report of project health metrics."""
if not (project := Project.objects.filter(key=f"www-project-{project_key}").first()):
raise Http404

if pdf := generate_latest_metrics_pdf(
ProjectHealthMetrics.get_latest_health_metrics().filter(project=project).first()
):
return FileResponse(
pdf,
as_attachment=True,
filename=f"{project_key}_health_metrics_report.pdf",
)

raise Http404
10 changes: 9 additions & 1 deletion backend/apps/owasp/api/internal/views/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,20 @@

from django.urls import path

from apps.owasp.api.internal.views.project_health_metrics import generate_overview_pdf
from apps.owasp.api.internal.views.project_health_metrics import (
generate_overview_pdf,
generate_project_health_metrics_pdf,
)

urlpatterns = [
path(
"project-health-metrics/overview/pdf/",
generate_overview_pdf,
name="project_health_metrics_overview_pdf",
),
path(
"project-health-metrics/<str:project_key>/pdf/",
generate_project_health_metrics_pdf,
name="project_health_metrics_pdf",
),
]

This file was deleted.

137 changes: 118 additions & 19 deletions backend/apps/owasp/utils/pdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,43 @@
from io import BytesIO

from django.utils import timezone
from reportlab.lib.pagesizes import letter
from reportlab.pdfgen.canvas import Canvas
from reportlab.platypus import Table, TableStyle

from apps.owasp.api.internal.nodes.project_health_stats import ProjectHealthStatsNode
from apps.owasp.models.project_health_metrics import ProjectHealthMetrics


def generate_metrics_overview_pdf() -> BytesIO:
def create_table(data, col_widths="*"):
"""Create a styled table for PDF generation."""
return Table(
data,
colWidths=col_widths,
style=TableStyle(
(
("BACKGROUND", (0, 0), (-1, 0), "#f2f2f2"),
("TEXTCOLOR", (0, 0), (-1, 0), "#000000"),
("ALIGN", (0, 0), (-1, -1), "CENTER"),
("FONTNAME", (0, 0), (-1, -1), "Helvetica"),
("BOTTOMPADDING", (0, 0), (-1, -1), 12),
("BACKGROUND", (0, 1), (-1, -1), "#ffffff"),
("GRID", (0, 0), (-1, -1), 1, "#dddddd"),
)
),
)


def generate_metrics_overview_pdf(metrics_stats: ProjectHealthStatsNode) -> BytesIO:
"""Generate a PDF overview of project health metrics.

Args:
metrics_stats: The project health stats node.

Returns:
BytesIO: PDF content as bytes.

"""
metrics_stats = ProjectHealthMetrics.get_stats()

buffer = BytesIO()
canvas = Canvas(buffer)
canvas.setFont("Helvetica", 12)
Expand Down Expand Up @@ -52,26 +74,103 @@ def generate_metrics_overview_pdf() -> BytesIO:
),
)

table = Table(
table_data,
colWidths="*",
style=TableStyle(
(
("BACKGROUND", (0, 0), (-1, 0), "lightgrey"),
("TEXTCOLOR", (0, 0), (-1, 0), "black"),
("ALIGN", (0, 0), (-1, -1), "CENTER"),
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
("BOTTOMPADDING", (0, 0), (-1, 0), 5),
("BACKGROUND", (0, 1), (-1, -1), "white"),
)
),
)
table.wrapOn(canvas, 400, 600)
table.drawOn(canvas, 100, 570)
table = create_table(table_data)
table.wrapOn(canvas, 400, 500)
table.drawOn(canvas, 100, 470)
canvas.drawCentredString(
300, 100, f"Generated on: {timezone.now().strftime('%Y-%m-%d %H:%M:%S')}"
)
canvas.showPage()
canvas.save()
buffer.seek(0)
return buffer


def generate_latest_metrics_pdf(metrics: ProjectHealthMetrics) -> BytesIO | None:
"""Generate a PDF report of the latest health metrics for a project.

Args:
metrics (ProjectHealthMetrics): The project health metrics.

Returns:
BytesIO: A buffer containing the generated PDF report.

"""
if not metrics:
return None

buffer = BytesIO()
pdf = Canvas(buffer, pagesize=letter)
pdf.setTitle(f"Health Metrics Report for {metrics.project.name}")
pdf.setFont("Helvetica", 12)
pdf.drawCentredString(300, 700, f"Health Metrics Report for {metrics.project.name}")
pdf.drawCentredString(
300, 680, f"Health Score: {metrics.score:.2f}" if metrics.score is not None else "N/A"
)
table_data = (
("Metric", "Value"),
("Project Age", f"{metrics.age_days}/{metrics.age_days_requirement} days"),
(
"Last Commit",
f"{metrics.last_commit_days}/{metrics.last_commit_days_requirement} days",
),
(
"Last Pull Request",
# To bypass ruff long line error
"/".join(
[
str(metrics.last_pull_request_days),
f"{metrics.last_pull_request_days_requirement} days",
]
),
),
(
"Last Release",
f"{metrics.last_release_days}/{metrics.last_release_days_requirement} days",
),
(
"OWASP Page Last Update",
# To bypass ruff long line error
"/".join(
[
str(metrics.owasp_page_last_update_days),
f"{metrics.owasp_page_last_update_days_requirement} days",
]
),
),
("Open/Total Issues", f"{metrics.open_issues_count}/{metrics.total_issues_count}"),
(
"Open/Total Pull Requests",
f"{metrics.open_pull_requests_count}/{metrics.total_pull_requests_count}",
),
(
"Recent/Total Releases",
f"{metrics.recent_releases_count}/{metrics.total_releases_count}",
),
("Forks", metrics.forks_count),
("Stars", metrics.stars_count),
(
"Unassigned/Unanswered Issues",
f"{metrics.unassigned_issues_count}/{metrics.unanswered_issues_count}",
),
("Contributors", metrics.contributors_count),
(
"Has funding policy issues",
"No" if metrics.is_funding_requirements_compliant else "Yes",
),
(
"Has leadership policy issues",
"No" if metrics.is_leader_requirements_compliant else "Yes",
),
)
table = create_table(table_data)
table.wrapOn(pdf, 500, 250)
table.drawOn(pdf, 50, 220)
pdf.drawCentredString(
300, 100, f"Report Generated on: {timezone.now().strftime('%Y-%m-%d %H:%M:%S')}"
)
pdf.showPage()
pdf.save()
buffer.seek(0)

return buffer
Loading