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
6 changes: 3 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ Before contributing, ensure you have the following installed:

#### `NEXT_PUBLIC_API_URL`

- **Description**: The base URL for the application's REST API.
- **Example Value**: `https://nest.owasp.org/api/`
- **Usage**: Used by frontend components to make REST API calls.
- **Description**: The base URL for the application's internal API.
- **Example Value**: `https://nest.owasp.org/`
- **Usage**: Used by frontend components to make API calls.

#### `NEXT_PUBLIC_CSRF_URL`

Expand Down
Empty file.
16 changes: 16 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,16 @@
"""Views for OWASP project health metrics."""

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

from apps.owasp.utils.pdf import generate_metrics_overview_pdf


@require_GET
def generate_overview_pdf(_request):
"""Generate a PDF overview of OWASP project health metrics."""
return FileResponse(
generate_metrics_overview_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.

Empty file.
77 changes: 77 additions & 0 deletions backend/apps/owasp/utils/pdf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"""PDF generation for OWASP project health metrics."""

from io import BytesIO

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

from apps.owasp.models.project_health_metrics import ProjectHealthMetrics


def generate_metrics_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}"
if metrics_stats.average_score is not None
else "N/A",
),
("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
2 changes: 2 additions & 0 deletions backend/settings/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
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
Expand All @@ -24,6 +25,7 @@
path("graphql/", csrf_protect(GraphQLView.as_view(schema=schema, graphiql=settings.DEBUG))),
path("api/v1/", api_v1.urls),
path("a/", admin.site.urls),
path("owasp/", include(owasp_urls)),
path("status/", get_status),
path("", include("apps.sitemap.urls")),
]
Expand Down

This file was deleted.

Empty file.
77 changes: 77 additions & 0 deletions backend/tests/apps/owasp/utils/pdf_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"""Test cases for OWASP project health metrics pdf generation."""

from unittest.mock import patch

from apps.owasp.api.internal.nodes.project_health_stats import ProjectHealthStatsNode
from apps.owasp.utils.pdf import generate_metrics_overview_pdf


class TestGenerateMetricsPDF:
@patch("apps.owasp.utils.pdf.ProjectHealthMetrics.get_stats")
@patch("apps.owasp.utils.pdf.Canvas")
@patch("apps.owasp.utils.pdf.Table")
@patch("apps.owasp.utils.pdf.TableStyle")
@patch("apps.owasp.utils.pdf.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}"
if metrics_stats.average_score is not None
else "N/A",
),
("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
generate_metrics_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()
4 changes: 2 additions & 2 deletions frontend/.env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
NEXT_PUBLIC_API_URL=http://localhost:8000/api/v1/
NEXT_PUBLIC_API_URL=http://localhost:8000/
NEXT_PUBLIC_CSRF_URL=http://localhost:8000/csrf/
NEXT_PUBLIC_ENVIRONMENT=local
NEXT_PUBLIC_GRAPHQL_URL=http://localhost:8000/graphql/
Expand All @@ -14,4 +14,4 @@ NEXT_SERVER_GITHUB_CLIENT_ID=
NEXT_SERVER_GITHUB_CLIENT_SECRET=
NEXT_SERVER_GRAPHQL_URL=http://backend:8000/graphql/
NEXTAUTH_SECRET=<your-nextauth-secret>
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_URL=http://localhost:3000/
Loading