Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 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
Empty file.
15 changes: 15 additions & 0 deletions backend/apps/owasp/api/internal/views/project_health_metrics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""Views for OWASP project health metrics."""

from django.http import FileResponse

from apps.owasp.models.project_health_metrics import ProjectHealthMetrics


def generate_overview_pdf(_request):
"""Generate a PDF overview of OWASP project health metrics."""
pdf = ProjectHealthMetrics.generate_overview_pdf()
return FileResponse(
pdf,
as_attachment=True,
filename="owasp_project_health_metrics_overview.pdf",
)
13 changes: 13 additions & 0 deletions backend/apps/owasp/api/internal/views/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"""URLs for OWASP project health metrics."""

from django.urls import path

from apps.owasp.api.internal.views.project_health_metrics import generate_overview_pdf

urlpatterns = [
path(
"project-health-metrics/overview/pdf/",
generate_overview_pdf,
name="project_health_metrics_overview_pdf",
),
]

This file was deleted.

67 changes: 67 additions & 0 deletions backend/apps/owasp/models/project_health_metrics.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
"""Project health metrics model."""

from io import BytesIO

from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.db.models.functions import ExtractMonth, TruncDate
from django.utils import timezone
from reportlab.pdfgen.canvas import Canvas
from reportlab.platypus import Table, TableStyle

from apps.common.models import BulkSaveModel, TimestampedModel
from apps.owasp.api.internal.nodes.project_health_stats import ProjectHealthStatsNode
Expand Down Expand Up @@ -156,6 +160,69 @@ def bulk_save(metrics: list, fields: list | None = None) -> None: # type: ignor
"""
BulkSaveModel.bulk_save(ProjectHealthMetrics, metrics, fields=fields)

@staticmethod
def generate_overview_pdf() -> BytesIO:
"""Generate a PDF overview of project health metrics.

Returns:
BytesIO: PDF content as bytes.

"""
metrics_stats = ProjectHealthMetrics.get_stats()

buffer = BytesIO()
canvas = Canvas(buffer)
canvas.setFont("Helvetica", 12)
canvas.setTitle("OWASP Project Health Metrics Overview")
canvas.drawCentredString(300, 800, "OWASP Project Health Metrics Overview")

table_data = [
["Metric", "Value"],
["Healthy Projects", f"{metrics_stats.projects_count_healthy}"],
["Unhealthy Projects", f"{metrics_stats.projects_count_unhealthy}"],
["Need Attention Projects", f"{metrics_stats.projects_count_need_attention}"],
["Average Score", f"{metrics_stats.average_score:.2f}"],
["Total Contributors", f"{metrics_stats.total_contributors:,}"],
["Total Forks", f"{metrics_stats.total_forks:,}"],
["Total Stars", f"{metrics_stats.total_stars:,}"],
[
"Healthy Projects Percentage",
f"%{metrics_stats.projects_percentage_healthy:.2f}",
],
[
"Need Attention Projects Percentage",
f"%{metrics_stats.projects_percentage_need_attention:.2f}",
],
[
"Unhealthy Projects Percentage",
f"%{metrics_stats.projects_percentage_unhealthy:.2f}",
],
]

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)
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

@staticmethod
def get_latest_health_metrics() -> models.QuerySet["ProjectHealthMetrics"]:
"""Get latest health metrics for each project.
Expand Down
2 changes: 2 additions & 0 deletions backend/settings/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@
from apps.core.api.internal.algolia import algolia_search
from apps.core.api.internal.csrf import get_csrf_token
from apps.core.api.internal.status import get_status
from apps.owasp.api.internal.views.urls import urlpatterns as owasp_urls
from apps.slack.apps import SlackConfig
from settings.api.v1 import api as api_v1
from settings.graphql import schema

urlpatterns = [
path("owasp/", include(owasp_urls)),
path("csrf/", get_csrf_token),
path("idx/", csrf_protect(algolia_search)),
path("graphql/", csrf_protect(GraphQLView.as_view(schema=schema, graphiql=settings.DEBUG))),
Expand Down
2 changes: 2 additions & 0 deletions backend/tests/apps/owasp/api/rest/v1/urls_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from apps.owasp.api.rest.v1.committee import router as committee_router
from apps.owasp.api.rest.v1.event import router as event_router
from apps.owasp.api.rest.v1.project import router as project_router
from apps.owasp.api.rest.v1.project_health_metrics import router as project_health_metrics_router
from apps.owasp.api.rest.v1.urls import router as main_router


Expand All @@ -15,6 +16,7 @@ class TestRouterRegistration:
"/committees": committee_router,
"/events": event_router,
"/projects": project_router,
"/project-health-metrics": project_health_metrics_router,
}

def test_all_routers_are_registered(self):
Expand Down

This file was deleted.

64 changes: 64 additions & 0 deletions backend/tests/apps/owasp/models/project_health_metrics_test.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
from unittest.mock import patch

import pytest
from django.core.exceptions import ValidationError
from django.utils import timezone

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

Expand Down Expand Up @@ -116,3 +119,64 @@ def test_handle_days_calculation(self, field_name, expected_days):
metrics.pull_request_last_created_at = self.FIXED_DATE

assert getattr(metrics, field_name) == expected_days

@patch("apps.owasp.models.project_health_metrics.ProjectHealthMetrics.get_stats")
@patch("apps.owasp.models.project_health_metrics.Canvas")
@patch("apps.owasp.models.project_health_metrics.Table")
@patch("apps.owasp.models.project_health_metrics.TableStyle")
@patch("apps.owasp.models.project_health_metrics.BytesIO")
def test_generate_overview_pdf(
self,
mock_bytes_io,
mock_table_style,
mock_table,
mock_canvas,
mock_get_stats,
):
"""Test that the command executes without errors."""
metrics_stats = ProjectHealthStatsNode(
projects_count_healthy=10,
projects_count_unhealthy=5,
projects_count_need_attention=3,
average_score=75.0,
total_contributors=150,
total_forks=200,
total_stars=300,
projects_percentage_healthy=66.67,
projects_percentage_need_attention=20.00,
projects_percentage_unhealthy=13.33,
monthly_overall_scores=[],
monthly_overall_scores_months=[],
)
table_data = [
["Metric", "Value"],
["Healthy Projects", f"{metrics_stats.projects_count_healthy}"],
["Unhealthy Projects", f"{metrics_stats.projects_count_unhealthy}"],
["Need Attention Projects", f"{metrics_stats.projects_count_need_attention}"],
["Average Score", f"{metrics_stats.average_score:.2f}"],
["Total Contributors", f"{metrics_stats.total_contributors:,}"],
["Total Forks", f"{metrics_stats.total_forks:,}"],
["Total Stars", f"{metrics_stats.total_stars:,}"],
["Healthy Projects Percentage", f"%{metrics_stats.projects_percentage_healthy:.2f}"],
[
"Need Attention Projects Percentage",
f"%{metrics_stats.projects_percentage_need_attention:.2f}",
],
[
"Unhealthy Projects Percentage",
f"%{metrics_stats.projects_percentage_unhealthy:.2f}",
],
]
mock_get_stats.return_value = metrics_stats
ProjectHealthMetrics.generate_overview_pdf()
mock_bytes_io.assert_called_once()
mock_canvas.assert_called_once_with(mock_bytes_io.return_value)
canvas = mock_canvas.return_value
mock_table.assert_called_once_with(
table_data, colWidths="*", style=mock_table_style.return_value
)
mock_table_style.assert_called_once()
mock_table.return_value.wrapOn.assert_called_once_with(canvas, 400, 600)
mock_table.return_value.drawOn.assert_called_once_with(canvas, 100, 570)
canvas.showPage.assert_called_once()
canvas.save.assert_called_once()
1 change: 1 addition & 0 deletions frontend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ NEXT_PUBLIC_ENVIRONMENT=local
NEXT_PUBLIC_GRAPHQL_URL=http://localhost:8000/graphql/
NEXT_PUBLIC_GTM_ID=
NEXT_PUBLIC_IDX_URL=http://localhost:8000/idx/
NEXT_PUBLIC_VIEWS_URL=http://localhost:8000/
NEXT_PUBLIC_IS_PROJECT_HEALTH_ENABLED=true
NEXT_PUBLIC_RELEASE_VERSION=
NEXT_PUBLIC_SENTRY_DSN=
Expand Down
Loading
Loading