Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
cd28fee
Added nest app and slack command for sponsor
abhayymishraa Feb 27, 2025
e7f54b1
Added test for sponsership model
abhayymishraa Feb 27, 2025
7e81614
Merge branch 'main' into feat/sponsorship-program
abhayymishraa Feb 27, 2025
faf9968
Resolved as per suggestions
abhayymishraa Mar 1, 2025
c448539
Resolved cr suggestions
abhayymishraa Mar 1, 2025
00da50e
Merge branch 'main' into feat/sponsorship-program
abhayymishraa Mar 1, 2025
c6c3ce7
Added suggestions by cr
abhayymishraa Mar 1, 2025
b17a378
Merge branch 'main' into feat/sponsorship-program
abhayymishraa Mar 2, 2025
a5ac816
Merge branch 'main' into feat/sponsorship-program
abhayymishraa Mar 3, 2025
482ec15
Merge branch 'main' into feat/sponsorship-program
abhayymishraa Mar 7, 2025
d8cc69e
Merge branch 'main' into feat/sponsorship-program
abhayymishraa Mar 13, 2025
f062c55
Merge branch 'main' into feat/sponsorship-program
arkid15r Mar 15, 2025
2949848
Merge branch 'main' into feat/sponsorship-program
abhayymishraa Mar 15, 2025
763cf8b
Merge branch 'main' into feat/sponsorship-program
abhayymishraa Mar 16, 2025
956c164
resolved suggestions
abhayymishraa Mar 16, 2025
ebcf410
resolved test
abhayymishraa Mar 16, 2025
78359aa
resolved conflict
abhayymishraa Mar 18, 2025
c272978
Merge branch 'main' into feat/sponsorship-program
abhayymishraa Mar 18, 2025
b09daae
Merge branch 'main' into feat/sponsorship-program
abhayymishraa Mar 20, 2025
19644b7
Merge branch 'main' into feat/sponsorship-program
abhayymishraa Mar 24, 2025
d8e444f
Merge branch 'main' into feat/sponsorship-program
abhayymishraa Apr 4, 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
67 changes: 67 additions & 0 deletions backend/apps/github/common.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
"""GitHub app common module."""

import logging
import os
from datetime import timedelta as td
from urllib.parse import urlparse

from django.core.exceptions import ValidationError
from django.utils import timezone
from github import Github
from github.GithubException import UnknownObjectException

from apps.github.models.issue import Issue
Expand All @@ -16,9 +20,72 @@
from apps.github.models.user import User
from apps.github.utils import check_owasp_site_repository

FETCH_ISSUE_ERROR = "Failed to fetch issue from GitHub: {error}"
MIN_PARTS_LENGTH = 4
VALID_PROJECT_ERROR = (
"Issue does not have a valid project and cannot be considered for sponsorship."
)
Comment on lines +23 to +27
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 adding a validation constant for malformed issue links.
While these constants help standardize error messages, there's no definition for something like INVALID_ISSUE_LINK_FORMAT if you plan to validate the link structure. Additionally, if MIN_PARTS_LENGTH remains unused, consider removing or referencing it when validating path_parts.



logger = logging.getLogger(__name__)


def sync_issue(issue_link):
"""Sync GitHub issue data."""
try:
return Issue.objects.get(url=issue_link)
except Issue.DoesNotExist:
pass

parsed_url = urlparse(issue_link)
path_parts = parsed_url.path.strip("/").split("/")

Comment on lines +40 to +42
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Validate URL path before extracting issue components.

The code parses the issue link but doesn't validate if it's a proper GitHub URL or if path_parts contains enough elements before accessing them. This can lead to IndexError exceptions when accessing path_parts[3] on line 46.

 parsed_url = urlparse(issue_link)
 path_parts = parsed_url.path.strip("/").split("/")
+
+if len(path_parts) < MIN_PARTS_LENGTH or path_parts[2] != "issues":
+    raise ValidationError(INVALID_ISSUE_LINK_FORMAT)

Note: The constant INVALID_ISSUE_LINK_FORMAT appears to be missing from the file and needs to be defined.


I'll start by running a shell script to search for the definitions of the constants MIN_PARTS_LENGTH and INVALID_ISSUE_LINK_FORMAT across the repository to verify if they exist. This will help us determine if the proposed validation changes are complete and if the missing constant needs to be defined.


🏁 Script executed:

#!/bin/bash
echo "Searching for MIN_PARTS_LENGTH defined in the repository..."
rg "MIN_PARTS_LENGTH" .

echo "Searching for INVALID_ISSUE_LINK_FORMAT defined in the repository..."
rg "INVALID_ISSUE_LINK_FORMAT" .

Length of output: 760


Critical: Validate GitHub Issue URL and Define Missing Constant

The updated validation logic correctly checks if the URL path contains enough parts and whether the issue keyword is present. However, while MIN_PARTS_LENGTH is defined in this file, the constant INVALID_ISSUE_LINK_FORMAT is missing and should be defined (or imported) to avoid runtime errors.

  • Action Required:
    • Define INVALID_ISSUE_LINK_FORMAT in backend/apps/github/common.py, for example:

      INVALID_ISSUE_LINK_FORMAT = "Invalid GitHub issue link format."
    • Ensure that the newly added validation snippet is integrated as follows:

       parsed_url = urlparse(issue_link)
       path_parts = parsed_url.path.strip("/").split("/")
      +
      +if len(path_parts) < MIN_PARTS_LENGTH or path_parts[2] != "issues":
      +    raise ValidationError(INVALID_ISSUE_LINK_FORMAT)

github_token = os.getenv("GITHUB_TOKEN")
github_client = Github(github_token)

issue_number = int(path_parts[3])
owner = path_parts[0]
repo_name = path_parts[1]

try:
gh_repo = github_client.get_repo(f"{owner}/{repo_name}")
gh_issue = gh_repo.get_issue(issue_number)
except Exception as error:
raise ValidationError(FETCH_ISSUE_ERROR.format(error=error)) from error

try:
author = User.objects.get(login=gh_issue.user.login)
except User.DoesNotExist:
author = User.update_data(gh_issue.user)

try:
repository = Repository.objects.get(node_id=gh_repo.id)
except Repository.DoesNotExist:
try:
repo_owner = User.objects.get(login=gh_repo.owner.login)
except User.DoesNotExist:
repo_owner = User.update_data(gh_repo.owner)
try:
organization = Organization.objects.get(node_id=gh_repo.organization.id)
except Organization.DoesNotExist:
organization = Organization.update_data(gh_repo.organization)

repository = Repository.update_data(
gh_repository=gh_repo,
commits=gh_repo.get_commits(),
contributors=gh_repo.get_contributors(),
languages=gh_repo.get_languages(),
organization=organization,
user=repo_owner,
)

if not repository.project:
logger.exception(VALID_PROJECT_ERROR)
return None

Comment on lines +82 to +85
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

Improve project validation error handling.

The code checks if the repository has a project, but only logs an error and returns None rather than raising the ValidationError with the message defined in the VALID_PROJECT_ERROR constant. This inconsistency between error handling approaches could make debugging harder.

 if not repository.project:
-    logger.exception(VALID_PROJECT_ERROR)
-    return None
+    logger.error(VALID_PROJECT_ERROR)
+    raise ValidationError(VALID_PROJECT_ERROR)
📝 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 not repository.project:
logger.exception(VALID_PROJECT_ERROR)
return None
if not repository.project:
logger.error(VALID_PROJECT_ERROR)
raise ValidationError(VALID_PROJECT_ERROR)

return Issue.update_data(gh_issue, author=author, repository=repository)


def sync_repository(gh_repository, organization=None, user=None):
"""Sync GitHub repository data.

Expand Down
Empty file added backend/apps/nest/__init__.py
Empty file.
11 changes: 11 additions & 0 deletions backend/apps/nest/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from django.contrib import admin

from apps.nest.models.sponsorship import Sponsorship


class SponsorshipAdmin(admin.ModelAdmin):
list_display = ("issue", "amount", "slack_user_id")
search_fields = ("issue__title", "slack_user_id")


admin.site.register(Sponsorship, SponsorshipAdmin)
5 changes: 5 additions & 0 deletions backend/apps/nest/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from django.apps import AppConfig


class NestConfig(AppConfig):
name = "apps.nest"
54 changes: 54 additions & 0 deletions backend/apps/nest/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Generated by Django 5.1.5 on 2025-03-16 07:43

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


class Migration(migrations.Migration):
initial = True

dependencies = [
("github", "0016_user_is_bot"),
]

operations = [
migrations.CreateModel(
name="Sponsorship",
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)),
(
"currency",
models.CharField(
choices=[("EUR", "Euro"), ("USD", "US Dollar")],
default="USD",
max_length=3,
),
),
("deadline_at", models.DateTimeField(blank=True, null=True)),
("amount", models.FloatField()),
("slack_user_id", models.CharField(max_length=100)),
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Validate the slack_user_id field.

Consider adding validation for the slack_user_id field format to ensure it matches Slack's user ID pattern.


🏁 Script executed:

#!/bin/bash
# Check if the Sponsorship model has validation for slack_user_id in the model definition

# Find the Sponsorship model definition
fd -e py -t f "sponsorship.py" | xargs grep -A 20 "class Sponsorship"

Length of output: 686


Action Required: Implement Slack User ID Validation

The Sponsorship model currently defines the slack_user_id field without any validation (see sponsorship.py), so the field does not enforce Slack’s expected user ID format. To avoid potential data quality issues, please add appropriate validation (for example, using Django's RegexValidator with a pattern matching Slack user IDs, such as one starting with "U" followed by alphanumeric characters). Note that changes should be made to the model definition; migration files will be updated accordingly in subsequent auto-generated migrations.

(
"issue",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="sponsorships",
to="github.issue",
),
),
],
options={
"verbose_name_plural": "Sponsorships",
"db_table": "nest_sponsorships",
},
),
]
Empty file.
Empty file.
74 changes: 74 additions & 0 deletions backend/apps/nest/models/sponsorship.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"""Nest app sponsorship model."""

from django.core.exceptions import ValidationError
from django.db import models
from django.utils import timezone

from apps.common.models import BulkSaveModel, TimestampedModel
from apps.github.models.issue import Issue

DEADLINE_FUTURE_ERROR = "Deadline must be in the future."
ISSUE_LINK_ERROR = "Issue link must belong to an OWASP repository."
PRICE_POSITIVE_ERROR = "Price must be a positive value."


class Sponsorship(BulkSaveModel, TimestampedModel):
"""Sponsorship model."""

class CurrencyType(models.TextChoices):
"""Currency type choices."""

EUR = "EUR", "Euro"
USD = "USD", "US Dollar"

class Meta:
db_table = "nest_sponsorships"
verbose_name_plural = "Sponsorships"

currency = models.CharField(
max_length=3, choices=CurrencyType.choices, default=CurrencyType.USD
)
deadline_at = models.DateTimeField(null=True, blank=True)
amount = models.FloatField()
slack_user_id = models.CharField(max_length=100)

issue = models.ForeignKey(
Issue,
on_delete=models.CASCADE,
related_name="sponsorships",
)

def __str__(self):
"""Sponsorship human readable representation."""
return f"Sponsorship for {self.issue.title} by {self.slack_user_id}"

def clean(self):
"""Validate model data."""
super().clean()

# Validate amount is positive
if self.amount <= 0:
raise ValidationError(PRICE_POSITIVE_ERROR)

# Validate deadline is in the future
if self.deadline_at and self.deadline_at < timezone.now():
raise ValidationError(DEADLINE_FUTURE_ERROR)

# Validate GitHub issue link belongs to OWASP
if not self.issue.url.startswith("https://github.com/OWASP"):
raise ValidationError(ISSUE_LINK_ERROR)

def save(self, *args, **kwargs):
"""Override save to run full validation."""
self.full_clean()
super().save(*args, **kwargs)

@staticmethod
def update_data(sponsorship, **kwargs):
"""Update sponsorship data with the provided fields."""
fields_to_update = ["amount", "deadline_at", "slack_user_id"]
for field in fields_to_update:
if field in kwargs:
setattr(sponsorship, field, kwargs[field])
sponsorship.save()
return sponsorship
Loading
Loading