Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
c319404
Creating a command to update metrics - initial step
ahmedxgouda Jun 2, 2025
a46b9e4
Add missing properties to the project model and update the metrics sc…
ahmedxgouda Jun 3, 2025
5984ea2
Merge branch 'main' into dashboard/health-evaluation
ahmedxgouda Jun 3, 2025
fdec122
Apply make-check and check-spelling
ahmedxgouda Jun 3, 2025
032abbd
Update tests
ahmedxgouda Jun 3, 2025
eca8635
Apply coderabbitai suggestion
ahmedxgouda Jun 3, 2025
23eb00a
Add is_funding_requirements_compliant to the metrics script
ahmedxgouda Jun 3, 2025
bcedde3
Apply coderabbitai suggestion
ahmedxgouda Jun 4, 2025
5bcd9b8
Add owasp_page_last_updated_at to the metrics
ahmedxgouda Jun 4, 2025
8110192
Refactor is_funding_requirements_compliant logic
ahmedxgouda Jun 4, 2025
58865cb
Add command to calculate score
ahmedxgouda Jun 4, 2025
8d6c3c0
Remove TODO
ahmedxgouda Jun 4, 2025
f7beaaf
Add metrics test
ahmedxgouda Jun 4, 2025
cbbd63f
Add metrics_score test
ahmedxgouda Jun 4, 2025
c515b51
Update metrics test
ahmedxgouda Jun 4, 2025
dfac905
Merge branch 'main' into dashboard/health-evaluation
ahmedxgouda Jun 4, 2025
9d30c8e
Merge branch 'main' into dashboard/health-evaluation
kasya Jun 6, 2025
3788253
Merge branch 'main' into dashboard/health-evaluation
ahmedxgouda Jun 7, 2025
fd0688d
Apply suggestions and add tests
ahmedxgouda Jun 7, 2025
5452beb
Rename score script to be plural
ahmedxgouda Jun 8, 2025
c85e2ea
Change the logic to create multiple ProjectHealthMetrics objects for …
ahmedxgouda Jun 8, 2025
acf0c66
Merge branch 'main' into dashboard/health-evaluation
ahmedxgouda Jun 8, 2025
56f3cf3
Apply coderabbitai suggestion
ahmedxgouda Jun 8, 2025
8845db3
Merge branch 'main' into dashboard/health-evaluation
ahmedxgouda Jun 10, 2025
9cea493
Merge branch 'main' into dashboard/health-evaluation
ahmedxgouda Jun 10, 2025
891074d
Make open issues count negative direction
ahmedxgouda Jun 11, 2025
f5b4514
Merge branch 'main' into dashboard/health-evaluation
ahmedxgouda Jun 11, 2025
9faf664
Evaluate the funding and project leaders requirement in the score and…
ahmedxgouda Jun 11, 2025
1e70ad7
Merge branch 'main' into dashboard/health-evaluation
kasya Jun 12, 2025
dc7adcd
Apply suggestions
ahmedxgouda Jun 12, 2025
8edde6d
Merge branch 'main' into dashboard/health-evaluation
ahmedxgouda Jun 12, 2025
c6a7061
Merge branch 'main' into dashboard/health-evaluation
ahmedxgouda Jun 12, 2025
a2eaf7a
Apply coderabbitai suggestion
ahmedxgouda Jun 12, 2025
b49f80d
Apply coderabbitai suggetion
ahmedxgouda Jun 12, 2025
4f24790
Fix tests
ahmedxgouda Jun 12, 2025
1702e50
Update code
arkid15r Jun 13, 2025
e4d661a
Merge branch 'main' into dashboard/health-evaluation
arkid15r Jun 13, 2025
8dabe94
Apply suggestions
ahmedxgouda Jun 13, 2025
1918428
Fix if condition
ahmedxgouda Jun 13, 2025
b787163
Merge branch 'main' into pr/ahmedxgouda/1550
arkid15r Jun 14, 2025
9c555c8
Update code
arkid15r Jun 14, 2025
5d1d1aa
Merge branch 'main' into dashboard/health-evaluation
arkid15r Jun 15, 2025
56231b9
Make older projects have higher scores
ahmedxgouda Jun 15, 2025
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: 6 additions & 2 deletions backend/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,15 @@ owasp-process-snapshots:
@CMD="python manage.py owasp_process_snapshots" $(MAKE) exec-backend-command

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

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

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

owasp-scrape-chapters:
Expand Down
26 changes: 25 additions & 1 deletion backend/apps/owasp/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,30 @@ def custom_field_name(self, obj) -> str:
custom_field_name.short_description = "Name"


class ProjectHealthMetricsAdmin(admin.ModelAdmin):
autocomplete_fields = ("project",)
list_filter = (
"project__level",
"nest_created_at",
)
list_display = (
"project",
"nest_created_at",
"score",
"contributors_count",
"stars_count",
"forks_count",
"open_issues_count",
"open_pull_requests_count",
"recent_releases_count",
)
search_fields = ("project__name",)

def project(self, obj):
"""Display project name."""
return obj.project.name if obj.project else "N/A"


class SnapshotAdmin(admin.ModelAdmin):
autocomplete_fields = (
"new_chapters",
Expand Down Expand Up @@ -229,7 +253,7 @@ class SponsorAdmin(admin.ModelAdmin):
admin.site.register(Event, EventAdmin)
admin.site.register(Post, PostAdmin)
admin.site.register(Project, ProjectAdmin)
admin.site.register(ProjectHealthMetrics)
admin.site.register(ProjectHealthMetrics, ProjectHealthMetricsAdmin)
admin.site.register(ProjectHealthRequirements)
admin.site.register(Snapshot, SnapshotAdmin)
admin.site.register(Sponsor, SponsorAdmin)
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""A command to update OWASP project health metrics."""

from django.core.management.base import BaseCommand

from apps.owasp.models.project import Project
from apps.owasp.models.project_health_metrics import ProjectHealthMetrics


class Command(BaseCommand):
help = "Update OWASP project health metrics."

def handle(self, *args, **options):
metric_project_field_mapping = {
"contributors_count": "contributors_count",
"created_at": "created_at",
"forks_count": "forks_count",
"is_funding_requirements_compliant": "is_funding_requirements_compliant",
"is_leader_requirements_compliant": "is_leader_requirements_compliant",
"last_committed_at": "pushed_at",
"last_released_at": "released_at",
"open_issues_count": "open_issues_count",
"open_pull_requests_count": "open_pull_requests_count",
"owasp_page_last_updated_at": "owasp_page_last_updated_at",
"pull_request_last_created_at": "pull_request_last_created_at",
"recent_releases_count": "recent_releases_count",
"stars_count": "stars_count",
"total_issues_count": "issues_count",
"total_pull_requests_count": "pull_requests_count",
"total_releases_count": "releases_count",
"unanswered_issues_count": "unanswered_issues_count",
"unassigned_issues_count": "unassigned_issues_count",
}
project_health_metrics = []
for project in Project.objects.all():
self.stdout.write(self.style.NOTICE(f"Evaluating metrics for project: {project.name}"))
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

self.style.NOTICE will raise AttributeError

Django’s color_style does not define a NOTICE style. Use one of the built-ins (SUCCESS, WARNING, ERROR, …) or output plain text.

- self.stdout.write(self.style.NOTICE(f"..."))
+ self.stdout.write(self.style.WARNING(f"..."))
📝 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
self.stdout.write(self.style.NOTICE(f"Evaluating metrics for project: {project.name}"))
for project in projects:
- self.stdout.write(self.style.NOTICE(f"Evaluating metrics for project: {project.name}"))
+ self.stdout.write(self.style.WARNING(f"Evaluating metrics for project: {project.name}"))
metrics = ProjectHealthMetrics(project=project)
# … rest of loop …
🤖 Prompt for AI Agents
In backend/apps/owasp/management/commands/owasp_update_project_health_metrics.py
at line 35, replace the use of self.style.NOTICE with a valid style such as
self.style.SUCCESS, self.style.WARNING, or self.style.ERROR, or simply output
the message as plain text without styling to avoid the AttributeError caused by
the undefined NOTICE style.

metrics = ProjectHealthMetrics(project=project)

# Update metrics based on requirements.
for metric_field, project_field in metric_project_field_mapping.items():
setattr(metrics, metric_field, getattr(project, project_field))

project_health_metrics.append(metrics)

ProjectHealthMetrics.bulk_save(project_health_metrics)
self.stdout.write(self.style.SUCCESS("Evaluated projects health metrics successfully. "))
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""A command to update OWASP project health metrics scores."""

from django.core.management.base import BaseCommand

from apps.owasp.models.project_health_metrics import ProjectHealthMetrics
from apps.owasp.models.project_health_requirements import ProjectHealthRequirements


class Command(BaseCommand):
help = "Update OWASP project health metrics score."

def handle(self, *args, **options):
forward_fields = {
"age_days": 6.0,
"contributors_count": 6.0,
"forks_count": 6.0,
"is_funding_requirements_compliant": 5.0,
"is_leader_requirements_compliant": 5.0,
"open_pull_requests_count": 6.0,
"recent_releases_count": 6.0,
"stars_count": 6.0,
"total_pull_requests_count": 6.0,
"total_releases_count": 6.0,
}
backward_fields = {
"last_commit_days": 6.0,
"last_pull_request_days": 6.0,
"last_release_days": 6.0,
"open_issues_count": 6.0,
"owasp_page_last_update_days": 6.0,
"unanswered_issues_count": 6.0,
"unassigned_issues_count": 6.0,
}

project_health_metrics = []
project_health_requirements = {
phr.level: phr for phr in ProjectHealthRequirements.objects.all()
}
for metric in ProjectHealthMetrics.objects.filter(
score__isnull=True,
).select_related(
"project",
):
# Calculate the score based on requirements.
self.stdout.write(
self.style.NOTICE(f"Updating score for project: {metric.project.name}")
)
Comment on lines +45 to +47
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

self.style.NOTICE will raise AttributeError in Django

Style doesn’t define a NOTICE attribute in any supported Django version. The command will crash as soon as it tries to log the message.

-            self.stdout.write(
-                self.style.NOTICE(f"Updating score for project: {metric.project.name}")
-            )
+            self.stdout.write(
+                self.style.WARNING(f"Updating score for project: {metric.project.name}")
+            )

Or drop styling altogether if you just need plain output.

📝 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
self.stdout.write(
self.style.NOTICE(f"Updating score for project: {metric.project.name}")
)
self.stdout.write(
- self.style.NOTICE(f"Updating score for project: {metric.project.name}")
+ self.style.WARNING(f"Updating score for project: {metric.project.name}")
)
🤖 Prompt for AI Agents
In
backend/apps/owasp/management/commands/owasp_update_project_health_metrics_scores.py
around lines 37 to 39, replace the use of self.style.NOTICE with a valid style
attribute such as self.style.NOTICE does not exist in Django and causes an
AttributeError. Use self.style.NOTICE should be changed to self.style.NOTICE or
simply remove styling by using self.stdout.write without styling to avoid the
crash.


requirements = project_health_requirements[metric.project.level]

score = 0.0
for field, weight in forward_fields.items():
if int(getattr(metric, field)) >= int(getattr(requirements, field)):
score += weight

for field, weight in backward_fields.items():
if int(getattr(metric, field)) <= int(getattr(requirements, field)):
score += weight

metric.score = score
project_health_metrics.append(metric)

ProjectHealthMetrics.bulk_save(project_health_metrics, fields=["score"])
self.stdout.write(
self.style.SUCCESS("Updated projects health metrics score successfully.")
)
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ class Command(BaseCommand):
"age_days": 15,
"contributors_count": 1,
"forks_count": 2,
"is_funding_requirements_compliant": True,
"is_leader_requirements_compliant": False,
"last_commit_days": 365,
"last_pull_request_days": 60,
"last_release_days": 365,
Expand All @@ -32,6 +34,8 @@ class Command(BaseCommand):
"age_days": 20,
"contributors_count": 3,
"forks_count": 5,
"is_funding_requirements_compliant": True,
"is_leader_requirements_compliant": True,
"last_commit_days": 270,
"last_pull_request_days": 45,
"last_release_days": 365,
Expand All @@ -50,6 +54,8 @@ class Command(BaseCommand):
"age_days": 30,
"contributors_count": 4,
"forks_count": 7,
"is_funding_requirements_compliant": True,
"is_leader_requirements_compliant": True,
"last_commit_days": 90,
"last_pull_request_days": 30,
"last_release_days": 180,
Expand All @@ -68,6 +74,8 @@ class Command(BaseCommand):
"age_days": 30,
"contributors_count": 5,
"forks_count": 10,
"is_funding_requirements_compliant": True,
"is_leader_requirements_compliant": True,
"last_commit_days": 180,
"last_pull_request_days": 30,
"last_release_days": 365,
Expand All @@ -84,59 +92,41 @@ class Command(BaseCommand):
},
}

def add_arguments(self, parser) -> None:
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)
return self.LEVEL_REQUIREMENTS.get(
level,
{
"age_days": 0,
"contributors_count": 0,
"forks_count": 0,
"is_funding_requirements_compliant": True,
"is_leader_requirements_compliant": True,
"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,
},
)

def handle(self, *args, **options) -> None:
"""Handle the command execution."""
level = options.get("level")

if level:
defaults = self.get_level_requirements(level)
requirements, created = ProjectHealthRequirements.objects.get_or_create(
level=level, defaults=defaults
for level_code, level_name in sorted(Project.ProjectLevel.choices):
_, created = ProjectHealthRequirements.objects.update_or_create(
level=level_code,
defaults=self.get_level_requirements(level_code),
)

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")
print(
f"{'Created' if created else 'Updated'} health requirements for "
f"{level_name} projects"
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 5.2.1 on 2025-06-03 14:47

from django.db import migrations


class Migration(migrations.Migration):
dependencies = [
("owasp", "0034_alter_chapter_leaders_and_more"),
]

operations = [
migrations.RenameField(
model_name="projecthealthmetrics",
old_name="total_pull_request_count",
new_name="total_pull_requests_count",
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Generated by Django 5.2.1 on 2025-06-08 02:58

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


class Migration(migrations.Migration):
dependencies = [
(
"owasp",
"0035_rename_total_pull_request_count_projecthealthmetrics_total_pull_requests_count",
),
]

operations = [
migrations.AlterField(
model_name="projecthealthmetrics",
name="score",
field=models.FloatField(
help_text="Project health score (0-100)",
null=True,
validators=[
django.core.validators.MinValueValidator(0.0),
django.core.validators.MaxValueValidator(100.0),
],
),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Generated by Django 5.2.1 on 2025-06-08 03:01

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


class Migration(migrations.Migration):
dependencies = [
("owasp", "0036_alter_projecthealthmetrics_score"),
]

operations = [
migrations.AlterField(
model_name="projecthealthmetrics",
name="project",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="health_metrics",
to="owasp.project",
),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 5.2.2 on 2025-06-12 23:19

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("owasp", "0037_alter_projecthealthmetrics_project"),
]

operations = [
migrations.RemoveField(
model_name="projecthealthmetrics",
name="is_project_leaders_requirements_compliant",
),
migrations.AddField(
model_name="projecthealthmetrics",
name="is_leaders_requirements_compliant",
field=models.BooleanField(
default=False, verbose_name="Is project leaders requirements compliant"
),
),
Comment on lines +11 to +22
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Remove-then-add pattern silently drops existing column values

RemoveField followed immediately by AddField resets every project’s compliance flag to the default False.
Unless intentional, swap the operations for a RenameField or add a RunPython step that backfills the new column from the old one before dropping it.

🤖 Prompt for AI Agents
In
backend/apps/owasp/migrations/0038_remove_projecthealthmetrics_is_project_leaders_requirements_compliant_and_more.py
around lines 11 to 22, the migration uses RemoveField followed by AddField which
causes existing data to be lost by resetting the column values to default. To
fix this, replace the RemoveField and AddField with a RenameField operation to
preserve existing data, or alternatively, add a RunPython data migration step
that copies values from the old field to the new field before removing the old
field.

]
Loading