-
-
Notifications
You must be signed in to change notification settings - Fork 72
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Added nest app and slack command for sponsorship #947
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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) |
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" |
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)), | ||
( | ||
"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", | ||
}, | ||
), | ||
] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
"""Nest app sponsorship model.""" | ||
|
||
from django.db import models | ||
|
||
from apps.common.models import BulkSaveModel, TimestampedModel | ||
from apps.github.models.issue import Issue | ||
|
||
|
||
class Sponsorship(BulkSaveModel, TimestampedModel): | ||
"""Sponsorship model.""" | ||
|
||
deadline_at = models.DateTimeField(null=True, blank=True) | ||
price_usd = models.FloatField() | ||
slack_user_id = models.CharField(max_length=100) | ||
|
||
issue = models.ForeignKey( | ||
Issue, | ||
on_delete=models.CASCADE, | ||
related_name="sponsorships", | ||
) | ||
|
||
class Meta: | ||
db_table = "nest_sponsorships" | ||
verbose_name_plural = "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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,12 +2,17 @@ | |
|
||
import logging | ||
import re | ||
from datetime import datetime | ||
from functools import lru_cache | ||
from html import escape as escape_html | ||
from urllib.parse import urljoin | ||
|
||
import requests | ||
import yaml | ||
from django.conf import settings | ||
from django.core.exceptions import ValidationError | ||
from django.utils import timezone | ||
from github import Github | ||
from lxml import html | ||
from requests.exceptions import RequestException | ||
|
||
|
@@ -16,6 +21,19 @@ | |
logger = logging.getLogger(__name__) | ||
|
||
|
||
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." | ||
|
||
|
||
def escape(content): | ||
"""Escape HTML content.""" | ||
return escape_html(content, quote=False) | ||
|
@@ -65,6 +83,48 @@ def get_news_data(limit=10, timeout=30): | |
return items | ||
|
||
|
||
def get_or_create_issue(issue_link): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This scope is to broad for /sponsor command's function. Let's split it and remove from here. |
||
"""Fetch or create an Issue instance from the GitHub API.""" | ||
from apps.github.models.issue import Issue | ||
from apps.github.models.repository import Repository | ||
from apps.github.models.user import User | ||
|
||
logger.info("Fetching or creating issue for link: %s", issue_link) | ||
|
||
# Extract repository owner, repo name, and issue number from the issue link | ||
# Example: https://github.com/OWASP/Nest/issues/XYZ | ||
parts = issue_link.strip("/").split("/") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use |
||
if ( | ||
len(parts) < MIN_PARTS_LENGTH | ||
or parts[GITHUB_COM_INDEX] != "github.com" | ||
or parts[ISSUES_INDEX] != "issues" | ||
): | ||
raise ValidationError(INVALID_ISSUE_LINK_FORMAT) | ||
|
||
try: | ||
return Issue.objects.get(url=issue_link) | ||
except Issue.DoesNotExist: | ||
pass | ||
|
||
github_client = Github(settings.GITHUB_TOKEN) | ||
issue_number = int(parts[6]) | ||
owner = parts[3] | ||
repo_name = parts[4] | ||
|
||
try: | ||
# Fetch the repository and issue from GitHub | ||
gh_repo = github_client.get_repo(f"{owner}/{repo_name}") | ||
gh_issue = gh_repo.get_issue(issue_number) | ||
repository = Repository.objects.get(name=repo_name) | ||
author = User.objects.get(login=gh_issue.user.login) | ||
Comment on lines
+118
to
+119
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These may not exist yet. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes what did you suggest create user and repository too ?(if it not exist in the database? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, we should create them first. |
||
|
||
# Update or create the issue in the database | ||
return Issue.update_data(gh_issue, author=author, repository=repository) | ||
except Exception as e: | ||
logger.exception("Failed to fetch issue from GitHub: %s") | ||
raise ValidationError(FETCH_ISSUE_ERROR.format(error=e)) from e | ||
Comment on lines
+104
to
+125
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be a part of |
||
|
||
|
||
@lru_cache | ||
def get_staff_data(timeout=30): | ||
"""Get staff data.""" | ||
|
@@ -132,3 +192,43 @@ def strip_markdown(text): | |
"""Strip markdown formatting.""" | ||
slack_link_pattern = re.compile(r"<(https?://[^|]+)\|([^>]+)>") | ||
return slack_link_pattern.sub(r"\2 (\1)", text).replace("*", "") | ||
|
||
|
||
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( | ||
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 | ||
|
||
|
||
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 | ||
|
||
|
||
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 | ||
Comment on lines
+197
to
+234
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These validators belong with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Snapshot model ? really? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i think you meant There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You're right. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Move to top