Skip to content

Commit ddab0b7

Browse files
ahmedxgoudakasyaarkid15r
authored
Create a health evaluation criteria (#1550)
* Creating a command to update metrics - initial step * Add missing properties to the project model and update the metrics script * Apply make-check and check-spelling * Update tests * Apply coderabbitai suggestion * Add is_funding_requirements_compliant to the metrics script * Apply coderabbitai suggestion * Add owasp_page_last_updated_at to the metrics * Refactor is_funding_requirements_compliant logic * Add command to calculate score * Remove TODO * Add metrics test * Add metrics_score test * Update metrics test * Apply suggestions and add tests * Rename score script to be plural * Change the logic to create multiple ProjectHealthMetrics objects for a project and apply suggestions * Apply coderabbitai suggestion * Make open issues count negative direction * Evaluate the funding and project leaders requirement in the score and add tests * Apply suggestions * Apply coderabbitai suggestion * Apply coderabbitai suggetion * Fix tests * Update code * Apply suggestions * Fix if condition * Update code * Make older projects have higher scores --------- Co-authored-by: Kate Golovanova <[email protected]> Co-authored-by: Arkadii Yakovets <[email protected]> Co-authored-by: Arkadii Yakovets <[email protected]>
1 parent f464620 commit ddab0b7

21 files changed

+693
-73
lines changed

backend/Makefile

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,11 +104,15 @@ owasp-process-snapshots:
104104
@CMD="python manage.py owasp_process_snapshots" $(MAKE) exec-backend-command
105105

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

110+
owasp-update-project-health-metrics-scores:
111+
@echo "Updating OWASP project health metrics score"
112+
@CMD="python manage.py owasp_update_project_health_metrics_scores" $(MAKE) exec-backend-command
113+
110114
owasp-update-project-health-requirements:
111-
@echo "Updating OWASP project health metrics"
115+
@echo "Updating OWASP project health requirements"
112116
@CMD="python manage.py owasp_update_project_health_requirements" $(MAKE) exec-backend-command
113117

114118
owasp-scrape-chapters:

backend/apps/owasp/admin.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,30 @@ def custom_field_name(self, obj) -> str:
164164
custom_field_name.short_description = "Name"
165165

166166

167+
class ProjectHealthMetricsAdmin(admin.ModelAdmin):
168+
autocomplete_fields = ("project",)
169+
list_filter = (
170+
"project__level",
171+
"nest_created_at",
172+
)
173+
list_display = (
174+
"project",
175+
"nest_created_at",
176+
"score",
177+
"contributors_count",
178+
"stars_count",
179+
"forks_count",
180+
"open_issues_count",
181+
"open_pull_requests_count",
182+
"recent_releases_count",
183+
)
184+
search_fields = ("project__name",)
185+
186+
def project(self, obj):
187+
"""Display project name."""
188+
return obj.project.name if obj.project else "N/A"
189+
190+
167191
class SnapshotAdmin(admin.ModelAdmin):
168192
autocomplete_fields = (
169193
"new_chapters",
@@ -229,7 +253,7 @@ class SponsorAdmin(admin.ModelAdmin):
229253
admin.site.register(Event, EventAdmin)
230254
admin.site.register(Post, PostAdmin)
231255
admin.site.register(Project, ProjectAdmin)
232-
admin.site.register(ProjectHealthMetrics)
256+
admin.site.register(ProjectHealthMetrics, ProjectHealthMetricsAdmin)
233257
admin.site.register(ProjectHealthRequirements)
234258
admin.site.register(Snapshot, SnapshotAdmin)
235259
admin.site.register(Sponsor, SponsorAdmin)
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"""A command to update OWASP project health metrics."""
2+
3+
from django.core.management.base import BaseCommand
4+
5+
from apps.owasp.models.project import Project
6+
from apps.owasp.models.project_health_metrics import ProjectHealthMetrics
7+
8+
9+
class Command(BaseCommand):
10+
help = "Update OWASP project health metrics."
11+
12+
def handle(self, *args, **options):
13+
metric_project_field_mapping = {
14+
"contributors_count": "contributors_count",
15+
"created_at": "created_at",
16+
"forks_count": "forks_count",
17+
"is_funding_requirements_compliant": "is_funding_requirements_compliant",
18+
"is_leader_requirements_compliant": "is_leader_requirements_compliant",
19+
"last_committed_at": "pushed_at",
20+
"last_released_at": "released_at",
21+
"open_issues_count": "open_issues_count",
22+
"open_pull_requests_count": "open_pull_requests_count",
23+
"owasp_page_last_updated_at": "owasp_page_last_updated_at",
24+
"pull_request_last_created_at": "pull_request_last_created_at",
25+
"recent_releases_count": "recent_releases_count",
26+
"stars_count": "stars_count",
27+
"total_issues_count": "issues_count",
28+
"total_pull_requests_count": "pull_requests_count",
29+
"total_releases_count": "releases_count",
30+
"unanswered_issues_count": "unanswered_issues_count",
31+
"unassigned_issues_count": "unassigned_issues_count",
32+
}
33+
project_health_metrics = []
34+
for project in Project.objects.all():
35+
self.stdout.write(self.style.NOTICE(f"Evaluating metrics for project: {project.name}"))
36+
metrics = ProjectHealthMetrics(project=project)
37+
38+
# Update metrics based on requirements.
39+
for metric_field, project_field in metric_project_field_mapping.items():
40+
setattr(metrics, metric_field, getattr(project, project_field))
41+
42+
project_health_metrics.append(metrics)
43+
44+
ProjectHealthMetrics.bulk_save(project_health_metrics)
45+
self.stdout.write(self.style.SUCCESS("Evaluated projects health metrics successfully. "))
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
"""A command to update OWASP project health metrics scores."""
2+
3+
from django.core.management.base import BaseCommand
4+
5+
from apps.owasp.models.project_health_metrics import ProjectHealthMetrics
6+
from apps.owasp.models.project_health_requirements import ProjectHealthRequirements
7+
8+
9+
class Command(BaseCommand):
10+
help = "Update OWASP project health metrics score."
11+
12+
def handle(self, *args, **options):
13+
forward_fields = {
14+
"age_days": 6.0,
15+
"contributors_count": 6.0,
16+
"forks_count": 6.0,
17+
"is_funding_requirements_compliant": 5.0,
18+
"is_leader_requirements_compliant": 5.0,
19+
"open_pull_requests_count": 6.0,
20+
"recent_releases_count": 6.0,
21+
"stars_count": 6.0,
22+
"total_pull_requests_count": 6.0,
23+
"total_releases_count": 6.0,
24+
}
25+
backward_fields = {
26+
"last_commit_days": 6.0,
27+
"last_pull_request_days": 6.0,
28+
"last_release_days": 6.0,
29+
"open_issues_count": 6.0,
30+
"owasp_page_last_update_days": 6.0,
31+
"unanswered_issues_count": 6.0,
32+
"unassigned_issues_count": 6.0,
33+
}
34+
35+
project_health_metrics = []
36+
project_health_requirements = {
37+
phr.level: phr for phr in ProjectHealthRequirements.objects.all()
38+
}
39+
for metric in ProjectHealthMetrics.objects.filter(
40+
score__isnull=True,
41+
).select_related(
42+
"project",
43+
):
44+
# Calculate the score based on requirements.
45+
self.stdout.write(
46+
self.style.NOTICE(f"Updating score for project: {metric.project.name}")
47+
)
48+
49+
requirements = project_health_requirements[metric.project.level]
50+
51+
score = 0.0
52+
for field, weight in forward_fields.items():
53+
if int(getattr(metric, field)) >= int(getattr(requirements, field)):
54+
score += weight
55+
56+
for field, weight in backward_fields.items():
57+
if int(getattr(metric, field)) <= int(getattr(requirements, field)):
58+
score += weight
59+
60+
metric.score = score
61+
project_health_metrics.append(metric)
62+
63+
ProjectHealthMetrics.bulk_save(project_health_metrics, fields=["score"])
64+
self.stdout.write(
65+
self.style.SUCCESS("Updated projects health metrics score successfully.")
66+
)

backend/apps/owasp/management/commands/owasp_update_project_health_requirements.py

Lines changed: 39 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ class Command(BaseCommand):
1414
"age_days": 15,
1515
"contributors_count": 1,
1616
"forks_count": 2,
17+
"is_funding_requirements_compliant": True,
18+
"is_leader_requirements_compliant": False,
1719
"last_commit_days": 365,
1820
"last_pull_request_days": 60,
1921
"last_release_days": 365,
@@ -32,6 +34,8 @@ class Command(BaseCommand):
3234
"age_days": 20,
3335
"contributors_count": 3,
3436
"forks_count": 5,
37+
"is_funding_requirements_compliant": True,
38+
"is_leader_requirements_compliant": True,
3539
"last_commit_days": 270,
3640
"last_pull_request_days": 45,
3741
"last_release_days": 365,
@@ -50,6 +54,8 @@ class Command(BaseCommand):
5054
"age_days": 30,
5155
"contributors_count": 4,
5256
"forks_count": 7,
57+
"is_funding_requirements_compliant": True,
58+
"is_leader_requirements_compliant": True,
5359
"last_commit_days": 90,
5460
"last_pull_request_days": 30,
5561
"last_release_days": 180,
@@ -68,6 +74,8 @@ class Command(BaseCommand):
6874
"age_days": 30,
6975
"contributors_count": 5,
7076
"forks_count": 10,
77+
"is_funding_requirements_compliant": True,
78+
"is_leader_requirements_compliant": True,
7179
"last_commit_days": 180,
7280
"last_pull_request_days": 30,
7381
"last_release_days": 365,
@@ -84,59 +92,41 @@ class Command(BaseCommand):
8492
},
8593
}
8694

87-
def add_arguments(self, parser) -> None:
88-
parser.add_argument(
89-
"--level",
90-
type=str,
91-
choices=[level[0] for level in Project.ProjectLevel.choices],
92-
help="Project level to set requirements for",
93-
)
94-
9595
def get_level_requirements(self, level):
9696
"""Get default requirements based on project level."""
97-
defaults = {
98-
"age_days": 0,
99-
"contributors_count": 0,
100-
"forks_count": 0,
101-
"last_commit_days": 0,
102-
"last_pull_request_days": 0,
103-
"last_release_days": 0,
104-
"open_issues_count": 0,
105-
"open_pull_requests_count": 0,
106-
"owasp_page_last_update_days": 0,
107-
"recent_releases_count": 0,
108-
"recent_releases_time_window_days": 0,
109-
"stars_count": 0,
110-
"total_pull_requests_count": 0,
111-
"total_releases_count": 0,
112-
"unanswered_issues_count": 0,
113-
"unassigned_issues_count": 0,
114-
}
115-
116-
return self.LEVEL_REQUIREMENTS.get(level, defaults)
97+
return self.LEVEL_REQUIREMENTS.get(
98+
level,
99+
{
100+
"age_days": 0,
101+
"contributors_count": 0,
102+
"forks_count": 0,
103+
"is_funding_requirements_compliant": True,
104+
"is_leader_requirements_compliant": True,
105+
"last_commit_days": 0,
106+
"last_pull_request_days": 0,
107+
"last_release_days": 0,
108+
"open_issues_count": 0,
109+
"open_pull_requests_count": 0,
110+
"owasp_page_last_update_days": 0,
111+
"recent_releases_count": 0,
112+
"recent_releases_time_window_days": 0,
113+
"stars_count": 0,
114+
"total_pull_requests_count": 0,
115+
"total_releases_count": 0,
116+
"unanswered_issues_count": 0,
117+
"unassigned_issues_count": 0,
118+
},
119+
)
117120

118121
def handle(self, *args, **options) -> None:
119122
"""Handle the command execution."""
120-
level = options.get("level")
121-
122-
if level:
123-
defaults = self.get_level_requirements(level)
124-
requirements, created = ProjectHealthRequirements.objects.get_or_create(
125-
level=level, defaults=defaults
123+
for level_code, level_name in sorted(Project.ProjectLevel.choices):
124+
_, created = ProjectHealthRequirements.objects.update_or_create(
125+
level=level_code,
126+
defaults=self.get_level_requirements(level_code),
126127
)
127128

128-
action = "Created" if created else "Updated"
129-
print(f"{action} health requirements for {requirements.get_level_display()} projects")
130-
else:
131-
for level_choice in Project.ProjectLevel.choices:
132-
level_code = level_choice[0]
133-
defaults = self.get_level_requirements(level_code)
134-
135-
requirements, created = ProjectHealthRequirements.objects.get_or_create(
136-
level=level_code, defaults=defaults
137-
)
138-
139-
if created:
140-
print(f"Created default health requirements for {level_choice[1]} projects")
141-
else:
142-
print(f"Health requirements already exist for {level_choice[1]} projects")
129+
print(
130+
f"{'Created' if created else 'Updated'} health requirements for "
131+
f"{level_name} projects"
132+
)
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Generated by Django 5.2.1 on 2025-06-03 14:47
2+
3+
from django.db import migrations
4+
5+
6+
class Migration(migrations.Migration):
7+
dependencies = [
8+
("owasp", "0034_alter_chapter_leaders_and_more"),
9+
]
10+
11+
operations = [
12+
migrations.RenameField(
13+
model_name="projecthealthmetrics",
14+
old_name="total_pull_request_count",
15+
new_name="total_pull_requests_count",
16+
),
17+
]
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Generated by Django 5.2.1 on 2025-06-08 02:58
2+
3+
import django.core.validators
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
dependencies = [
9+
(
10+
"owasp",
11+
"0035_rename_total_pull_request_count_projecthealthmetrics_total_pull_requests_count",
12+
),
13+
]
14+
15+
operations = [
16+
migrations.AlterField(
17+
model_name="projecthealthmetrics",
18+
name="score",
19+
field=models.FloatField(
20+
help_text="Project health score (0-100)",
21+
null=True,
22+
validators=[
23+
django.core.validators.MinValueValidator(0.0),
24+
django.core.validators.MaxValueValidator(100.0),
25+
],
26+
),
27+
),
28+
]
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Generated by Django 5.2.1 on 2025-06-08 03:01
2+
3+
import django.db.models.deletion
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
dependencies = [
9+
("owasp", "0036_alter_projecthealthmetrics_score"),
10+
]
11+
12+
operations = [
13+
migrations.AlterField(
14+
model_name="projecthealthmetrics",
15+
name="project",
16+
field=models.ForeignKey(
17+
on_delete=django.db.models.deletion.CASCADE,
18+
related_name="health_metrics",
19+
to="owasp.project",
20+
),
21+
),
22+
]
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Generated by Django 5.2.2 on 2025-06-12 23:19
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
dependencies = [
8+
("owasp", "0037_alter_projecthealthmetrics_project"),
9+
]
10+
11+
operations = [
12+
migrations.RemoveField(
13+
model_name="projecthealthmetrics",
14+
name="is_project_leaders_requirements_compliant",
15+
),
16+
migrations.AddField(
17+
model_name="projecthealthmetrics",
18+
name="is_leaders_requirements_compliant",
19+
field=models.BooleanField(
20+
default=False, verbose_name="Is project leaders requirements compliant"
21+
),
22+
),
23+
]

0 commit comments

Comments
 (0)