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
4 changes: 3 additions & 1 deletion backend/apps/owasp/index/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ class ProjectIndex(IndexBase):
"idx_custom_tags",
"idx_description",
"idx_forks_count",
"idx_issues_count",
"idx_health_score",
"idx_is_active",
"idx_issues_count",
"idx_key",
"idx_languages",
"idx_leaders",
Expand Down Expand Up @@ -48,6 +49,7 @@ class ProjectIndex(IndexBase):
"indexLanguages": ["en"],
"customRanking": [
"desc(idx_level_raw)",
"desc(idx_health_score)",
"desc(idx_stars_count)",
"desc(idx_contributors_count)",
"desc(idx_forks_count)",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@

from django.core.management.base import BaseCommand

from apps.owasp.models.project import Project
from apps.owasp.models.enums.project import ProjectLevel
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: {
ProjectLevel.INCUBATOR: {
"age_days": 15,
"contributors_count": 1,
"forks_count": 2,
Expand All @@ -30,7 +30,7 @@ class Command(BaseCommand):
"unanswered_issues_count": 5,
"unassigned_issues_count": 5,
},
Project.ProjectLevel.LAB: {
ProjectLevel.LAB: {
"age_days": 20,
"contributors_count": 3,
"forks_count": 5,
Expand All @@ -50,7 +50,7 @@ class Command(BaseCommand):
"unanswered_issues_count": 4,
"unassigned_issues_count": 4,
},
Project.ProjectLevel.PRODUCTION: {
ProjectLevel.PRODUCTION: {
"age_days": 30,
"contributors_count": 4,
"forks_count": 7,
Expand All @@ -70,7 +70,7 @@ class Command(BaseCommand):
"unanswered_issues_count": 2,
"unassigned_issues_count": 2,
},
Project.ProjectLevel.FLAGSHIP: {
ProjectLevel.FLAGSHIP: {
"age_days": 30,
"contributors_count": 5,
"forks_count": 10,
Expand Down Expand Up @@ -120,7 +120,7 @@ def get_level_requirements(self, level):

def handle(self, *args, **options) -> None:
"""Handle the command execution."""
for level_code, level_name in sorted(Project.ProjectLevel.choices):
for level_code, level_name in sorted(ProjectLevel.choices):
_, created = ProjectHealthRequirements.objects.update_or_create(
level=level_code,
defaults=self.get_level_requirements(level_code),
Expand Down
Empty file.
34 changes: 34 additions & 0 deletions backend/apps/owasp/models/enums/project.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""Enums for OWASP projects."""

from django.db.models import TextChoices

Comment on lines +3 to +4
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

Add gettext_lazy import for translatable labels

Django convention is to wrap human-readable enum labels in gettext_lazy so they can be picked up by the i18n tooling.
Importing it here avoids repeating the import in every consumer that needs translations.

-from django.db.models import TextChoices
+from django.db.models import TextChoices
+from django.utils.translation import gettext_lazy as _
📝 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
from django.db.models import TextChoices
from django.db.models import TextChoices
from django.utils.translation import gettext_lazy as _
🤖 Prompt for AI Agents
In backend/apps/owasp/models/enums/project.py around lines 3 to 4, the
gettext_lazy function from django.utils.translation is not imported, which is
needed to wrap human-readable enum labels for translation. Add the import
statement for gettext_lazy at the top of the file to enable wrapping enum labels
for i18n support and avoid repeated imports in consumers.


class ProjectType(TextChoices):
"""Enum for OWASP project types."""

# These projects provide tools, libraries, and frameworks that can be leveraged by
# developers to enhance the security of their applications.
CODE = "code", "Code"

# These projects seek to communicate information or raise awareness about a topic in
# application security. Note that documentation projects should focus on an online-first
# deliverable, where appropriate, but can take any media form.
DOCUMENTATION = "documentation", "Documentation"

# Some projects fall outside the above categories. Most are created to offer OWASP
# operational support.
OTHER = "other", "Other"

# These are typically software or utilities that help developers and security
# professionals test, secure, or monitor applications.
TOOL = "tool", "Tool"
Comment on lines +11 to +24
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

Wrap enum labels in _() for internationalisation

The second element of each TextChoices tuple is shown in forms / admin and should be localisable.
Without _() they’ll be extracted as plain strings, leaving them untranslated for non-English users.

-    CODE = "code", "Code"
+    CODE = "code", _("Code")
 ...
-    DOCUMENTATION = "documentation", "Documentation"
+    DOCUMENTATION = "documentation", _("Documentation")
 ...
-    OTHER = "other", "Other"
+    OTHER = "other", _("Other")
 ...
-    TOOL = "tool", "Tool"
+    TOOL = "tool", _("Tool")
@@
-    OTHER = "other", "Other"
-    INCUBATOR = "incubator", "Incubator"
-    LAB = "lab", "Lab"
-    PRODUCTION = "production", "Production"
-    FLAGSHIP = "flagship", "Flagship"
+    OTHER = "other", _("Other")
+    INCUBATOR = "incubator", _("Incubator")
+    LAB = "lab", _("Lab")
+    PRODUCTION = "production", _("Production")
+    FLAGSHIP = "flagship", _("Flagship")

Also applies to: 30-34

🤖 Prompt for AI Agents
In backend/apps/owasp/models/enums/project.py around lines 11 to 24 and also
lines 30 to 34, the second element of each TextChoices tuple is not wrapped in
the _() function for internationalization. To fix this, import the _ function
from django.utils.translation and wrap each label string (the second element in
the tuple) with _() to enable translation in forms and admin interfaces.



class ProjectLevel(TextChoices):
"""Enum for OWASP project levels."""

OTHER = "other", "Other"
INCUBATOR = "incubator", "Incubator"
LAB = "lab", "Lab"
PRODUCTION = "production", "Production"
FLAGSHIP = "flagship", "Flagship"
8 changes: 8 additions & 0 deletions backend/apps/owasp/models/mixins/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

from __future__ import annotations

from django.conf import settings

from apps.common.utils import join_values
from apps.github.models.repository_contributor import RepositoryContributor
from apps.owasp.models.mixins.common import RepositoryBasedEntityModelMixin
Expand Down Expand Up @@ -34,6 +36,12 @@ def idx_forks_count(self) -> int:
"""Return forks count for indexing."""
return self.forks_count

@property
def idx_health_score(self) -> float | None:
"""Return health score for indexing."""
# TODO(arkid15r): Enable real health score in production when ready.
return 100 if settings.ENVIRONMENT == "Production" else self.health_score

@property
def idx_is_active(self) -> bool:
"""Return active status for indexing."""
Expand Down
69 changes: 28 additions & 41 deletions backend/apps/owasp/models/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@
from apps.github.models.pull_request import PullRequest
from apps.github.models.release import Release
from apps.owasp.models.common import RepositoryBasedEntityModel
from apps.owasp.models.enums.project import ProjectLevel, ProjectType
from apps.owasp.models.managers.project import ActiveProjectManager
from apps.owasp.models.mixins.project import ProjectIndexMixin
from apps.owasp.models.project_health_metrics import ProjectHealthMetrics


class Project(
Expand All @@ -39,31 +41,6 @@ class Meta:
]
verbose_name_plural = "Projects"

class ProjectLevel(models.TextChoices):
OTHER = "other", "Other"
INCUBATOR = "incubator", "Incubator"
LAB = "lab", "Lab"
PRODUCTION = "production", "Production"
FLAGSHIP = "flagship", "Flagship"

class ProjectType(models.TextChoices):
# These projects provide tools, libraries, and frameworks that can be leveraged by
# developers to enhance the security of their applications.
CODE = "code", "Code"

# These projects seek to communicate information or raise awareness about a topic in
# application security. Note that documentation projects should focus on an online-first
# deliverable, where appropriate, but can take any media form.
DOCUMENTATION = "documentation", "Documentation"

# Some projects fall outside the above categories. Most are created to offer OWASP
# operational support.
OTHER = "other", "Other"

# These are typically software or utilities that help developers and security
# professionals test, secure, or monitor applications.
TOOL = "tool", "Tool"

level = models.CharField(
verbose_name="Level",
max_length=20,
Expand Down Expand Up @@ -132,15 +109,20 @@ def __str__(self) -> str:
"""Project human readable representation."""
return f"{self.name or self.key}"

@property
def health_score(self) -> float | None:
"""Return project health score."""
return self.last_health_metrics.score if self.last_health_metrics else None

@property
def is_code_type(self) -> bool:
"""Indicate whether project has CODE type."""
return self.type == self.ProjectType.CODE
return self.type == ProjectType.CODE

@property
def is_documentation_type(self) -> bool:
"""Indicate whether project has DOCUMENTATION type."""
return self.type == self.ProjectType.DOCUMENTATION
return self.type == ProjectType.DOCUMENTATION

@property
def is_funding_requirements_compliant(self) -> bool:
Expand All @@ -157,7 +139,7 @@ def is_leader_requirements_compliant(self) -> bool:
@property
def is_tool_type(self) -> bool:
"""Indicate whether project has TOOL type."""
return self.type == self.ProjectType.TOOL
return self.type == ProjectType.TOOL

@property
def issues(self):
Expand All @@ -173,6 +155,18 @@ def issues_count(self) -> int:
"""Return count of issues."""
return self.issues.count()

@property
def last_health_metrics(self) -> ProjectHealthMetrics | None:
"""Return last health metrics for the project."""
return (
ProjectHealthMetrics.objects.filter(project=self).order_by("-nest_created_at").first()
)

@property
def leaders_count(self) -> int:
"""Return the count of leaders."""
return len(self.leaders_raw)

@property
def nest_key(self) -> str:
"""Get Nest key."""
Expand All @@ -183,11 +177,6 @@ def nest_url(self) -> str:
"""Get Nest URL for project."""
return get_absolute_url(f"projects/{self.nest_key}")

@property
def leaders_count(self) -> int:
"""Return the count of leaders."""
return len(self.leaders_raw)

@property
def open_issues(self):
"""Return open issues."""
Expand Down Expand Up @@ -298,20 +287,18 @@ def from_github(self, repository) -> None:
project_level = project_metadata.get("level")
if project_level:
level_mapping = {
2: self.ProjectLevel.INCUBATOR,
3: self.ProjectLevel.LAB,
3.5: self.ProjectLevel.PRODUCTION,
4: self.ProjectLevel.FLAGSHIP,
2: ProjectLevel.INCUBATOR,
3: ProjectLevel.LAB,
3.5: ProjectLevel.PRODUCTION,
4: ProjectLevel.FLAGSHIP,
}
self.level = level_mapping.get(project_level) or self.ProjectLevel.OTHER
self.level = level_mapping.get(project_level) or ProjectLevel.OTHER
self.level_raw = project_level

# Type.
project_type = project_metadata.get("type")
if project_type:
self.type = (
project_type if project_type in self.ProjectType.values else self.ProjectType.OTHER
)
self.type = project_type if project_type in ProjectType.values else ProjectType.OTHER
self.type_raw = project_type

self.created_at = repository.created_at
Expand Down
4 changes: 2 additions & 2 deletions backend/apps/owasp/models/project_health_requirements.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from django.db import models

from apps.common.models import TimestampedModel
from apps.owasp.models.project import Project
from apps.owasp.models.enums.project import ProjectLevel


class ProjectHealthRequirements(TimestampedModel):
Expand All @@ -16,7 +16,7 @@ class Meta:

level = models.CharField(
max_length=10,
choices=Project.ProjectLevel.choices,
choices=ProjectLevel.choices,
unique=True,
verbose_name="Project Level",
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from django.core.management.base import CommandError

from apps.owasp.management.commands.owasp_update_project_health_requirements import Command
from apps.owasp.models.project import Project
from apps.owasp.models.enums.project import ProjectLevel
from apps.owasp.models.project_health_requirements import ProjectHealthRequirements


Expand Down Expand Up @@ -61,11 +61,11 @@ def test_handle_exception(self):
@pytest.mark.parametrize(
"level",
[
Project.ProjectLevel.FLAGSHIP,
Project.ProjectLevel.INCUBATOR,
Project.ProjectLevel.LAB,
Project.ProjectLevel.PRODUCTION,
Project.ProjectLevel.OTHER,
ProjectLevel.FLAGSHIP,
ProjectLevel.INCUBATOR,
ProjectLevel.LAB,
ProjectLevel.PRODUCTION,
ProjectLevel.OTHER,
],
)
def test_get_level_requirements(self, level):
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import pytest
from django.core.exceptions import ValidationError

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


class TestProjectHealthRequirementsModel:
"""Unit tests for ProjectHealthRequirements model validation and behavior."""

VALID_LEVELS = Project.ProjectLevel.values
VALID_LEVELS = ProjectLevel.values
INVALID_LEVEL = "invalid_level"
POSITIVE_INTEGER_FIELDS = [
"contributors_count",
Expand All @@ -32,11 +32,11 @@ class TestProjectHealthRequirementsModel:
@pytest.mark.parametrize(
("level", "expected"),
[
(Project.ProjectLevel.FLAGSHIP, "Health Requirements for Flagship Projects"),
(Project.ProjectLevel.INCUBATOR, "Health Requirements for Incubator Projects"),
(Project.ProjectLevel.LAB, "Health Requirements for Lab Projects"),
(Project.ProjectLevel.OTHER, "Health Requirements for Other Projects"),
(Project.ProjectLevel.PRODUCTION, "Health Requirements for Production Projects"),
(ProjectLevel.FLAGSHIP, "Health Requirements for Flagship Projects"),
(ProjectLevel.INCUBATOR, "Health Requirements for Incubator Projects"),
(ProjectLevel.LAB, "Health Requirements for Lab Projects"),
(ProjectLevel.OTHER, "Health Requirements for Other Projects"),
(ProjectLevel.PRODUCTION, "Health Requirements for Production Projects"),
("", "Health Requirements for Projects"),
],
)
Expand Down
23 changes: 12 additions & 11 deletions backend/tests/apps/owasp/models/project_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from apps.github.models.repository import Repository
from apps.github.models.user import User
from apps.owasp.models.enums.project import ProjectLevel, ProjectType
from apps.owasp.models.project import Project


Expand Down Expand Up @@ -36,9 +37,9 @@ def test_project_str(self, level, project_type, name, key, expected_str):
@pytest.mark.parametrize(
("project_type", "expected_result"),
[
(Project.ProjectType.CODE, True),
(Project.ProjectType.DOCUMENTATION, False),
(Project.ProjectType.TOOL, False),
(ProjectType.CODE, True),
(ProjectType.DOCUMENTATION, False),
(ProjectType.TOOL, False),
],
)
def test_is_code_type(self, project_type, expected_result):
Expand All @@ -48,9 +49,9 @@ def test_is_code_type(self, project_type, expected_result):
@pytest.mark.parametrize(
("project_type", "expected_result"),
[
(Project.ProjectType.CODE, False),
(Project.ProjectType.DOCUMENTATION, True),
(Project.ProjectType.TOOL, False),
(ProjectType.CODE, False),
(ProjectType.DOCUMENTATION, True),
(ProjectType.TOOL, False),
],
)
def test_is_documentation_type(self, project_type, expected_result):
Expand All @@ -60,9 +61,9 @@ def test_is_documentation_type(self, project_type, expected_result):
@pytest.mark.parametrize(
("project_type", "expected_result"),
[
(Project.ProjectType.CODE, False),
(Project.ProjectType.DOCUMENTATION, False),
(Project.ProjectType.TOOL, True),
(ProjectType.CODE, False),
(ProjectType.DOCUMENTATION, False),
(ProjectType.TOOL, True),
],
)
def test_is_tool_type(self, project_type, expected_result):
Expand Down Expand Up @@ -127,6 +128,6 @@ def test_from_github(self):
)

assert project.created_at == owasp_repository.created_at
assert project.level == Project.ProjectLevel.LAB
assert project.type == Project.ProjectType.TOOL
assert project.level == ProjectLevel.LAB
assert project.type == ProjectType.TOOL
assert project.updated_at == owasp_repository.updated_at