Skip to content
Draft
Show file tree
Hide file tree
Changes from 6 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
59 changes: 59 additions & 0 deletions backend/apps/github/common.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
"""GitHub app common module."""

import logging
import os
from urllib.parse import urlparse

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

from apps.github.models.issue import Issue
Expand All @@ -13,9 +17,64 @@
from apps.github.models.user import User
from apps.github.utils import check_owasp_site_repository

INVALID_ISSUE_LINK_FORMAT = "Invalid GitHub issue link format."
FETCH_ISSUE_ERROR = "Failed to fetch issue from GitHub: {error}"
MIN_PARTS_LENGTH = 4


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("/")
if len(path_parts) < MIN_PARTS_LENGTH or path_parts[2] != "issues":
raise ValidationError(INVALID_ISSUE_LINK_FORMAT)
Copy link
Collaborator

Choose a reason for hiding this comment

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

The validation should happen upon saving the model instance. This function should accept a clean link w/o worrying about validation errors.


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]

gh_repo = github_client.get_repo(f"{owner}/{repo_name}")
gh_issue = gh_repo.get_issue(issue_number)
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,
)

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


def sync_repository(gh_repository, organization=None, user=None):
"""Sync GitHub repository data."""
entity_key = gh_repository.name.lower()
Expand Down
2 changes: 1 addition & 1 deletion backend/apps/github/models/issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ def generate_summary(self, open_ai=None, max_tokens=500):

prompt = (
Prompt.get_github_issue_documentation_project_summary()
if self.project.is_documentation_type
if self.project and self.project.is_documentation_type
Copy link
Collaborator

Choose a reason for hiding this comment

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

This also should be a part of validation on an upper level. If issue doesn't have a project it probably shouldn't be considered for sponsorship.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

when I tried to delete the nest from the local database, it couldn't generate the summary. That's result is that issue was not created. This might be because I don't have the credentials for OpenAI.
could you look into it and tell how can we fix that ?

else Prompt.get_github_issue_project_summary()
)
if not prompt:
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", "price_usd", "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"
13 changes: 13 additions & 0 deletions backend/apps/nest/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"""Nest app constants."""

ISSUES_INDEX = 5
GITHUB_COM_INDEX = 2
MIN_PARTS_LENGTH = 4

DEADLINE_FORMAT_ERROR = "Invalid deadline format. Use YYYY-MM-DD or YYYY-MM-DD HH:MM."
DEADLINE_FUTURE_ERROR = "Deadline must be in the future."
FETCH_ISSUE_ERROR = "Failed to fetch issue from GitHub: {error}"
INVALID_ISSUE_LINK_FORMAT = "Invalid GitHub issue link format."
ISSUE_LINK_ERROR = "Issue link must belong to an OWASP repository."
PRICE_POSITIVE_ERROR = "Price must be a positive value."
PRICE_VALID_ERROR = "Price must be a valid number."
Copy link
Collaborator

Choose a reason for hiding this comment

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

Narrow down the scope -- add to a separate constants only something that you need in multiple modules.

46 changes: 46 additions & 0 deletions backend/apps/nest/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Generated by Django 5.1.5 on 2025-02-27 05:33

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)),
("deadline_at", models.DateTimeField(blank=True, null=True)),
("price_usd", 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.
89 changes: 89 additions & 0 deletions backend/apps/nest/models/sponsorship.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
"""Nest app sponsorship model."""

from datetime import datetime

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
from apps.nest.constants import (
DEADLINE_FORMAT_ERROR,
DEADLINE_FUTURE_ERROR,
ISSUE_LINK_ERROR,
PRICE_POSITIVE_ERROR,
PRICE_VALID_ERROR,
)


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

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

deadline_at = models.DateTimeField(null=True, blank=True)
price_usd = models.FloatField()
Copy link
Collaborator

Choose a reason for hiding this comment

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

Let's rename price_usd to amount and add currency (EUR, USD with USD default)

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}"

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

@staticmethod
def validate_deadline(deadline_str):
"""Validate that the deadline is in a valid datetime format."""
try:
# Try parsing the deadline in YYYY-MM-DD format
deadline = datetime.strptime(deadline_str, "%Y-%m-%d").replace(
Copy link
Collaborator

Choose a reason for hiding this comment

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

Consider using python-dateutil parser for better date recognition

tzinfo=timezone.get_current_timezone()
)
except ValueError:
try:
# Try parsing the deadline in YYYY-MM-DD HH:MM format
deadline = datetime.strptime(deadline_str, "%Y-%m-%d %H:%M").replace(
tzinfo=timezone.get_current_timezone()
)
except ValueError as e:
raise ValidationError(DEADLINE_FORMAT_ERROR) from e

if deadline < timezone.now():
raise ValidationError(DEADLINE_FUTURE_ERROR)

return deadline

@staticmethod
def validate_github_issue_link(issue_link):
"""Validate that the issue link belongs to a valid OWASP-related repository."""
if not issue_link.startswith("https://github.com/OWASP"):
raise ValidationError(ISSUE_LINK_ERROR)
return issue_link

@staticmethod
def validate_price(price):
"""Validate that the price is a positive float value."""
try:
price = float(price)
if price <= 0:
raise ValidationError(PRICE_POSITIVE_ERROR)
except ValueError as e:
raise ValidationError(PRICE_VALID_ERROR) from e
return price
Loading