-
-
Notifications
You must be signed in to change notification settings - Fork 256
Added nest app and slack command for sponsorship #947
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
base: main
Are you sure you want to change the base?
Changes from 3 commits
cd28fee
e7f54b1
7e81614
faf9968
c448539
00da50e
c6c3ce7
b17a378
a5ac816
482ec15
d8cc69e
f062c55
2949848
763cf8b
956c164
ebcf410
78359aa
c272978
b09daae
19644b7
d8e444f
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): | ||
|
||
| """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("/") | ||
|
||
| 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) | ||
|
||
|
|
||
| # 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 | ||
|
||
|
|
||
|
|
||
| @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 | ||
|
||
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.
💡 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:
Length of output: 686
Action Required: Implement Slack User ID Validation
The Sponsorship model currently defines the
slack_user_idfield without any validation (seesponsorship.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'sRegexValidatorwith 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.