Skip to content
8 changes: 8 additions & 0 deletions backend/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,14 @@ owasp-process-snapshots:
@echo "Processing OWASP snapshots"
@CMD="python manage.py owasp_process_snapshots" $(MAKE) exec-backend-command

owasp-update-project-health-metrics:
@echo "Updating OWASP project health requirements"
@CMD="python manage.py owasp_update_project_health_metrics" $(MAKE) exec-backend-command

owasp-update-project-health-requirements:
@echo "Updating OWASP project health metrics"
@CMD="python manage.py owasp_update_project_health_requirements" $(MAKE) exec-backend-command

owasp-scrape-chapters:
@echo "Scraping OWASP site chapters data"
@CMD="python manage.py owasp_scrape_chapters" $(MAKE) exec-backend-command
Expand Down
4 changes: 4 additions & 0 deletions backend/apps/owasp/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
from apps.owasp.models.committee import Committee
from apps.owasp.models.event import Event
from apps.owasp.models.project import Project
from apps.owasp.models.project_health_metrics import ProjectHealthMetrics
from apps.owasp.models.project_health_requirements import ProjectHealthRequirements
from apps.owasp.models.snapshot import Snapshot


Expand Down Expand Up @@ -141,4 +143,6 @@ class SnapshotAdmin(admin.ModelAdmin):
admin.site.register(Committee, CommitteeAdmin)
admin.site.register(Event, EventAdmin)
admin.site.register(Project, ProjectAdmin)
admin.site.register(ProjectHealthMetrics)
admin.site.register(ProjectHealthRequirements)
admin.site.register(Snapshot, SnapshotAdmin)
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
"""A command to set thresholds of OWASP project health requirements."""

from django.core.management.base import BaseCommand

from apps.owasp.models.project import Project
from apps.owasp.models.project_health_requirements import ProjectHealthRequirements


class Command(BaseCommand):
help = "Set project health requirements for each level."

LEVEL_REQUIREMENTS = {
Project.ProjectLevel.INCUBATOR: {
"age_days": 15,
"contributors_count": 1,
"forks_count": 2,
"last_commit_days": 365,
"last_pull_request_days": 60,
"last_release_days": 365,
"open_issues_count": 10,
"open_pull_requests_count": 5,
"owasp_page_last_update_days": 60,
"recent_releases_count": 1,
"recent_releases_time_window_days": 120,
"stars_count": 10,
"total_pull_requests_count": 5,
"total_releases_count": 2,
"unanswered_issues_count": 5,
"unassigned_issues_count": 5,
},
Project.ProjectLevel.LAB: {
"age_days": 20,
"contributors_count": 3,
"forks_count": 5,
"last_commit_days": 270,
"last_pull_request_days": 45,
"last_release_days": 365,
"open_issues_count": 8,
"open_pull_requests_count": 4,
"owasp_page_last_update_days": 45,
"recent_releases_count": 1,
"recent_releases_time_window_days": 90,
"stars_count": 25,
"total_pull_requests_count": 10,
"total_releases_count": 3,
"unanswered_issues_count": 4,
"unassigned_issues_count": 4,
},
Project.ProjectLevel.PRODUCTION: {
"age_days": 30,
"contributors_count": 4,
"forks_count": 7,
"last_commit_days": 90,
"last_pull_request_days": 30,
"last_release_days": 180,
"open_issues_count": 5,
"open_pull_requests_count": 3,
"owasp_page_last_update_days": 30,
"recent_releases_count": 2,
"recent_releases_time_window_days": 60,
"stars_count": 40,
"total_pull_requests_count": 15,
"total_releases_count": 4,
"unanswered_issues_count": 2,
"unassigned_issues_count": 2,
},
Project.ProjectLevel.FLAGSHIP: {
"age_days": 30,
"contributors_count": 5,
"forks_count": 10,
"last_commit_days": 180,
"last_pull_request_days": 30,
"last_release_days": 365,
"open_issues_count": 5,
"open_pull_requests_count": 3,
"owasp_page_last_update_days": 30,
"recent_releases_count": 2,
"recent_releases_time_window_days": 90,
"stars_count": 50,
"total_pull_requests_count": 20,
"total_releases_count": 5,
"unanswered_issues_count": 3,
"unassigned_issues_count": 3,
},
}

def add_arguments(self, parser):
parser.add_argument(
"--level",
type=str,
choices=[level[0] for level in Project.ProjectLevel.choices],
help="Project level to set requirements for",
)

def get_level_requirements(self, level):
"""Get default requirements based on project level."""
defaults = {
"age_days": 0,
"contributors_count": 0,
"forks_count": 0,
"last_commit_days": 0,
"last_pull_request_days": 0,
"last_release_days": 0,
"open_issues_count": 0,
"open_pull_requests_count": 0,
"owasp_page_last_update_days": 0,
"recent_releases_count": 0,
"recent_releases_time_window_days": 0,
"stars_count": 0,
"total_pull_requests_count": 0,
"total_releases_count": 0,
"unanswered_issues_count": 0,
"unassigned_issues_count": 0,
}

return self.LEVEL_REQUIREMENTS.get(level, defaults)

def handle(self, *args, **options):
level = options.get("level")

if level:
defaults = self.get_level_requirements(level)
requirements, created = ProjectHealthRequirements.objects.get_or_create(
level=level, defaults=defaults
)

action = "Created" if created else "Updated"
print(f"{action} health requirements for {requirements.get_level_display()} projects")
Comment on lines +121 to +128
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider updating existing requirements.

The command checks if requirements exist, but it doesn't update them if the values in LEVEL_REQUIREMENTS have changed. Consider adding logic to update existing records with the latest requirement values.

         if level:
             defaults = self.get_level_requirements(level)
             requirements, created = ProjectHealthRequirements.objects.get_or_create(
                 level=level, defaults=defaults
             )
+            
+            if not created:
+                # Update the existing record with the latest requirements
+                for key, value in defaults.items():
+                    setattr(requirements, key, value)
+                requirements.save()

             action = "Created" if created else "Updated"
             print(f"{action} health requirements for {requirements.get_level_display()} projects")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if level:
defaults = self.get_level_requirements(level)
requirements, created = ProjectHealthRequirements.objects.get_or_create(
level=level, defaults=defaults
)
action = "Created" if created else "Updated"
print(f"{action} health requirements for {requirements.get_level_display()} projects")
if level:
defaults = self.get_level_requirements(level)
requirements, created = ProjectHealthRequirements.objects.get_or_create(
level=level, defaults=defaults
)
if not created:
# Update the existing record with the latest requirements
for key, value in defaults.items():
setattr(requirements, key, value)
requirements.save()
action = "Created" if created else "Updated"
print(f"{action} health requirements for {requirements.get_level_display()} projects")

else:
for level_choice in Project.ProjectLevel.choices:
level_code = level_choice[0]
defaults = self.get_level_requirements(level_code)

requirements, created = ProjectHealthRequirements.objects.get_or_create(
level=level_code, defaults=defaults
)

if created:
print(f"Created default health requirements for {level_choice[1]} projects")
else:
print(f"Health requirements already exist for {level_choice[1]} projects")
124 changes: 124 additions & 0 deletions backend/apps/owasp/migrations/0016_projecthealthmetrics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# Generated by Django 5.1.6 on 2025-02-28 11:57

import django.core.validators
import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("owasp", "0015_snapshot"),
]

operations = [
migrations.CreateModel(
name="ProjectHealthMetrics",
fields=[
(
"id",
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
("nest_created_at", models.DateTimeField(auto_now_add=True)),
("nest_updated_at", models.DateTimeField(auto_now=True)),
(
"contributors_count",
models.PositiveIntegerField(default=0, verbose_name="Contributors"),
),
(
"created_at",
models.DateTimeField(blank=True, null=True, verbose_name="Created at"),
),
("forks_count", models.PositiveIntegerField(default=0, verbose_name="Forks")),
(
"is_funding_requirements_compliant",
models.BooleanField(
default=False, verbose_name="Is funding requirements compliant"
),
),
(
"is_project_leaders_requirements_compliant",
models.BooleanField(
default=False, verbose_name="Is project leaders requirements compliant"
),
),
(
"last_released_at",
models.DateTimeField(blank=True, null=True, verbose_name="Last released at"),
),
(
"last_committed_at",
models.DateTimeField(blank=True, null=True, verbose_name="Last committed at"),
),
(
"open_issues_count",
models.PositiveIntegerField(default=0, verbose_name="Open issues"),
),
(
"open_pull_requests_count",
models.PositiveIntegerField(default=0, verbose_name="Open pull requests"),
),
(
"owasp_page_last_updated_at",
models.DateTimeField(
blank=True, null=True, verbose_name="OWASP page last updated at"
),
),
(
"pull_request_last_created_at",
models.DateTimeField(
blank=True, null=True, verbose_name="Pull request last created at"
),
),
(
"recent_releases_count",
models.PositiveIntegerField(default=0, verbose_name="Recent releases"),
),
(
"score",
models.FloatField(
default=0.0,
help_text="Project health score (0-100)",
validators=[
django.core.validators.MinValueValidator(0.0),
django.core.validators.MaxValueValidator(100.0),
],
),
),
("stars_count", models.PositiveIntegerField(default=0, verbose_name="Stars")),
(
"total_issues_count",
models.PositiveIntegerField(default=0, verbose_name="Total issues"),
),
(
"total_pull_request_count",
models.PositiveIntegerField(default=0, verbose_name="Total pull requests"),
),
(
"total_releases_count",
models.PositiveIntegerField(default=0, verbose_name="Total releases"),
),
(
"unanswered_issues_count",
models.PositiveIntegerField(default=0, verbose_name="Unanswered issues"),
),
(
"unassigned_issues_count",
models.PositiveIntegerField(default=0, verbose_name="Unassigned issues"),
),
(
"project",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
related_name="health_metrics",
to="owasp.project",
),
),
],
options={
"verbose_name_plural": "Project Health Metrics",
"db_table": "owasp_project_health_metrics",
},
),
]
Loading