Skip to content

Commit faf9968

Browse files
committed
Resolved as per suggestions
1 parent 7e81614 commit faf9968

File tree

7 files changed

+134
-117
lines changed

7 files changed

+134
-117
lines changed

backend/apps/github/common.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
"""GitHub app common module."""
22

33
import logging
4+
import os
5+
from urllib.parse import urlparse
46

7+
from django.core.exceptions import ValidationError
8+
from github import Github
59
from github.GithubException import UnknownObjectException
610

711
from apps.github.models.issue import Issue
@@ -13,9 +17,64 @@
1317
from apps.github.models.user import User
1418
from apps.github.utils import check_owasp_site_repository
1519

20+
INVALID_ISSUE_LINK_FORMAT = "Invalid GitHub issue link format."
21+
FETCH_ISSUE_ERROR = "Failed to fetch issue from GitHub: {error}"
22+
MIN_PARTS_LENGTH = 4
23+
24+
1625
logger = logging.getLogger(__name__)
1726

1827

28+
def sync_issue(issue_link):
29+
"""Sync GitHub issue data."""
30+
try:
31+
return Issue.objects.get(url=issue_link)
32+
except Issue.DoesNotExist:
33+
pass
34+
35+
parsed_url = urlparse(issue_link)
36+
path_parts = parsed_url.path.strip("/").split("/")
37+
if len(path_parts) < MIN_PARTS_LENGTH or path_parts[2] != "issues":
38+
raise ValidationError(INVALID_ISSUE_LINK_FORMAT)
39+
40+
github_token = os.getenv("GITHUB_TOKEN")
41+
github_client = Github(github_token)
42+
43+
issue_number = int(path_parts[3])
44+
owner = path_parts[0]
45+
repo_name = path_parts[1]
46+
47+
gh_repo = github_client.get_repo(f"{owner}/{repo_name}")
48+
gh_issue = gh_repo.get_issue(issue_number)
49+
try:
50+
author = User.objects.get(login=gh_issue.user.login)
51+
except User.DoesNotExist:
52+
author = User.update_data(gh_issue.user)
53+
54+
try:
55+
repository = Repository.objects.get(node_id=gh_repo.id)
56+
except Repository.DoesNotExist:
57+
try:
58+
owner = User.objects.get(login=gh_repo.owner.login)
59+
except User.DoesNotExist:
60+
owner = User.update_data(gh_repo.owner)
61+
try:
62+
organization = Organization.objects.get(node_id=gh_repo.organization.id)
63+
except Organization.DoesNotExist:
64+
organization = Organization.update_data(gh_repo.organization)
65+
66+
repository = Repository.update_data(
67+
gh_repository=gh_repo,
68+
commits=gh_repo.get_commits(),
69+
contributors=gh_repo.get_contributors(),
70+
languages=gh_repo.get_languages(),
71+
organization=organization,
72+
user=owner,
73+
)
74+
75+
return Issue.update_data(gh_issue, author=author, repository=repository)
76+
77+
1978
def sync_repository(gh_repository, organization=None, user=None):
2079
"""Sync GitHub repository data."""
2180
entity_key = gh_repository.name.lower()

backend/apps/github/models/issue.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ def generate_summary(self, open_ai=None, max_tokens=500):
151151

152152
prompt = (
153153
Prompt.get_github_issue_documentation_project_summary()
154-
if self.project.is_documentation_type
154+
if self.project and self.project.is_documentation_type
155155
else Prompt.get_github_issue_project_summary()
156156
)
157157
if not prompt:

backend/apps/nest/constants.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
"""Nest app constants."""
2+
3+
ISSUES_INDEX = 5
4+
GITHUB_COM_INDEX = 2
5+
MIN_PARTS_LENGTH = 4
6+
7+
DEADLINE_FORMAT_ERROR = "Invalid deadline format. Use YYYY-MM-DD or YYYY-MM-DD HH:MM."
8+
DEADLINE_FUTURE_ERROR = "Deadline must be in the future."
9+
FETCH_ISSUE_ERROR = "Failed to fetch issue from GitHub: {error}"
10+
INVALID_ISSUE_LINK_FORMAT = "Invalid GitHub issue link format."
11+
ISSUE_LINK_ERROR = "Issue link must belong to an OWASP repository."
12+
PRICE_POSITIVE_ERROR = "Price must be a positive value."
13+
PRICE_VALID_ERROR = "Price must be a valid number."

backend/apps/nest/models/sponsorship.py

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,29 @@
11
"""Nest app sponsorship model."""
22

3+
from datetime import datetime
4+
5+
from django.core.exceptions import ValidationError
36
from django.db import models
7+
from django.utils import timezone
48

59
from apps.common.models import BulkSaveModel, TimestampedModel
610
from apps.github.models.issue import Issue
11+
from apps.nest.constants import (
12+
DEADLINE_FORMAT_ERROR,
13+
DEADLINE_FUTURE_ERROR,
14+
ISSUE_LINK_ERROR,
15+
PRICE_POSITIVE_ERROR,
16+
PRICE_VALID_ERROR,
17+
)
718

819

920
class Sponsorship(BulkSaveModel, TimestampedModel):
1021
"""Sponsorship model."""
1122

23+
class Meta:
24+
db_table = "nest_sponsorships"
25+
verbose_name_plural = "Sponsorships"
26+
1227
deadline_at = models.DateTimeField(null=True, blank=True)
1328
price_usd = models.FloatField()
1429
slack_user_id = models.CharField(max_length=100)
@@ -19,10 +34,6 @@ class Sponsorship(BulkSaveModel, TimestampedModel):
1934
related_name="sponsorships",
2035
)
2136

22-
class Meta:
23-
db_table = "nest_sponsorships"
24-
verbose_name_plural = "Sponsorships"
25-
2637
def __str__(self):
2738
"""Sponsorship human readable representation."""
2839
return f"Sponsorship for {self.issue.title} by {self.slack_user_id}"
@@ -36,3 +47,43 @@ def update_data(sponsorship, **kwargs):
3647
setattr(sponsorship, field, kwargs[field])
3748
sponsorship.save()
3849
return sponsorship
50+
51+
@staticmethod
52+
def validate_deadline(deadline_str):
53+
"""Validate that the deadline is in a valid datetime format."""
54+
try:
55+
# Try parsing the deadline in YYYY-MM-DD format
56+
deadline = datetime.strptime(deadline_str, "%Y-%m-%d").replace(
57+
tzinfo=timezone.get_current_timezone()
58+
)
59+
except ValueError:
60+
try:
61+
# Try parsing the deadline in YYYY-MM-DD HH:MM format
62+
deadline = datetime.strptime(deadline_str, "%Y-%m-%d %H:%M").replace(
63+
tzinfo=timezone.get_current_timezone()
64+
)
65+
except ValueError as e:
66+
raise ValidationError(DEADLINE_FORMAT_ERROR) from e
67+
68+
if deadline < timezone.now():
69+
raise ValidationError(DEADLINE_FUTURE_ERROR)
70+
71+
return deadline
72+
73+
@staticmethod
74+
def validate_github_issue_link(issue_link):
75+
"""Validate that the issue link belongs to a valid OWASP-related repository."""
76+
if not issue_link.startswith("https://github.com/OWASP"):
77+
raise ValidationError(ISSUE_LINK_ERROR)
78+
return issue_link
79+
80+
@staticmethod
81+
def validate_price(price):
82+
"""Validate that the price is a positive float value."""
83+
try:
84+
price = float(price)
85+
if price <= 0:
86+
raise ValidationError(PRICE_POSITIVE_ERROR)
87+
except ValueError as e:
88+
raise ValidationError(PRICE_VALID_ERROR) from e
89+
return price

backend/apps/slack/commands/sponsor.py

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,7 @@
88

99
from apps.common.constants import NL
1010
from apps.slack.apps import SlackConfig
11-
from apps.slack.utils import (
12-
get_or_create_issue,
13-
get_text,
14-
validate_deadline,
15-
validate_github_issue_link,
16-
validate_price,
17-
)
11+
from apps.slack.utils import get_text
1812

1913
logger = logging.getLogger(__name__)
2014

@@ -30,6 +24,7 @@
3024

3125
def sponsor_handler(ack, command, client):
3226
"""Slack /sponsor command handler."""
27+
from apps.github.common import sync_issue
3328
from apps.nest.models.sponsorship import Sponsorship
3429

3530
ack()
@@ -56,12 +51,12 @@ def validate_command_format(parts):
5651
if len(parts) > TIME_INDEX:
5752
deadline_str += " " + parts[TIME_INDEX]
5853

59-
validate_github_issue_link(issue_link)
60-
validated_price = validate_price(price)
61-
deadline = validate_deadline(deadline_str) if deadline_str else None
54+
Sponsorship.validate_github_issue_link(issue_link)
55+
validated_price = Sponsorship.validate_price(price)
56+
deadline = Sponsorship.validate_deadline(deadline_str) if deadline_str else None
6257

6358
with transaction.atomic():
64-
issue = get_or_create_issue(issue_link)
59+
issue = sync_issue(issue_link)
6560
sponsorship, created = Sponsorship.objects.get_or_create(
6661
issue=issue,
6762
defaults={

backend/apps/slack/utils.py

Lines changed: 0 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,12 @@
22

33
import logging
44
import re
5-
from datetime import datetime
65
from functools import lru_cache
76
from html import escape as escape_html
87
from urllib.parse import urljoin
98

109
import requests
1110
import yaml
12-
from django.conf import settings
13-
from django.core.exceptions import ValidationError
14-
from django.utils import timezone
15-
from github import Github
1611
from lxml import html
1712
from requests.exceptions import RequestException
1813

@@ -21,19 +16,6 @@
2116
logger = logging.getLogger(__name__)
2217

2318

24-
ISSUES_INDEX = 5
25-
GITHUB_COM_INDEX = 2
26-
MIN_PARTS_LENGTH = 4
27-
28-
DEADLINE_FORMAT_ERROR = "Invalid deadline format. Use YYYY-MM-DD or YYYY-MM-DD HH:MM."
29-
DEADLINE_FUTURE_ERROR = "Deadline must be in the future."
30-
FETCH_ISSUE_ERROR = "Failed to fetch issue from GitHub: {error}"
31-
INVALID_ISSUE_LINK_FORMAT = "Invalid GitHub issue link format."
32-
ISSUE_LINK_ERROR = "Issue link must belong to an OWASP repository."
33-
PRICE_POSITIVE_ERROR = "Price must be a positive value."
34-
PRICE_VALID_ERROR = "Price must be a valid number."
35-
36-
3719
def escape(content):
3820
"""Escape HTML content."""
3921
return escape_html(content, quote=False)
@@ -83,48 +65,6 @@ def get_news_data(limit=10, timeout=30):
8365
return items
8466

8567

86-
def get_or_create_issue(issue_link):
87-
"""Fetch or create an Issue instance from the GitHub API."""
88-
from apps.github.models.issue import Issue
89-
from apps.github.models.repository import Repository
90-
from apps.github.models.user import User
91-
92-
logger.info("Fetching or creating issue for link: %s", issue_link)
93-
94-
# Extract repository owner, repo name, and issue number from the issue link
95-
# Example: https://github.com/OWASP/Nest/issues/XYZ
96-
parts = issue_link.strip("/").split("/")
97-
if (
98-
len(parts) < MIN_PARTS_LENGTH
99-
or parts[GITHUB_COM_INDEX] != "github.com"
100-
or parts[ISSUES_INDEX] != "issues"
101-
):
102-
raise ValidationError(INVALID_ISSUE_LINK_FORMAT)
103-
104-
try:
105-
return Issue.objects.get(url=issue_link)
106-
except Issue.DoesNotExist:
107-
pass
108-
109-
github_client = Github(settings.GITHUB_TOKEN)
110-
issue_number = int(parts[6])
111-
owner = parts[3]
112-
repo_name = parts[4]
113-
114-
try:
115-
# Fetch the repository and issue from GitHub
116-
gh_repo = github_client.get_repo(f"{owner}/{repo_name}")
117-
gh_issue = gh_repo.get_issue(issue_number)
118-
repository = Repository.objects.get(name=repo_name)
119-
author = User.objects.get(login=gh_issue.user.login)
120-
121-
# Update or create the issue in the database
122-
return Issue.update_data(gh_issue, author=author, repository=repository)
123-
except Exception as e:
124-
logger.exception("Failed to fetch issue from GitHub: %s")
125-
raise ValidationError(FETCH_ISSUE_ERROR.format(error=e)) from e
126-
127-
12868
@lru_cache
12969
def get_staff_data(timeout=30):
13070
"""Get staff data."""
@@ -192,43 +132,3 @@ def strip_markdown(text):
192132
"""Strip markdown formatting."""
193133
slack_link_pattern = re.compile(r"<(https?://[^|]+)\|([^>]+)>")
194134
return slack_link_pattern.sub(r"\2 (\1)", text).replace("*", "")
195-
196-
197-
def validate_deadline(deadline_str):
198-
"""Validate that the deadline is in a valid datetime format."""
199-
try:
200-
# Try parsing the deadline in YYYY-MM-DD format
201-
deadline = datetime.strptime(deadline_str, "%Y-%m-%d").replace(
202-
tzinfo=timezone.get_current_timezone()
203-
)
204-
except ValueError:
205-
try:
206-
# Try parsing the deadline in YYYY-MM-DD HH:MM format
207-
deadline = datetime.strptime(deadline_str, "%Y-%m-%d %H:%M").replace(
208-
tzinfo=timezone.get_current_timezone()
209-
)
210-
except ValueError as e:
211-
raise ValidationError(DEADLINE_FORMAT_ERROR) from e
212-
213-
if deadline < timezone.now():
214-
raise ValidationError(DEADLINE_FUTURE_ERROR)
215-
216-
return deadline
217-
218-
219-
def validate_github_issue_link(issue_link):
220-
"""Validate that the issue link belongs to a valid OWASP-related repository."""
221-
if not issue_link.startswith("https://github.com/OWASP"):
222-
raise ValidationError(ISSUE_LINK_ERROR)
223-
return issue_link
224-
225-
226-
def validate_price(price):
227-
"""Validate that the price is a positive float value."""
228-
try:
229-
price = float(price)
230-
if price <= 0:
231-
raise ValidationError(PRICE_POSITIVE_ERROR)
232-
except ValueError as e:
233-
raise ValidationError(PRICE_VALID_ERROR) from e
234-
return price

backend/settings/base.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ class Base(Configuration):
1919

2020
ALLOWED_HOSTS = values.ListValue()
2121
DEBUG = False
22-
GITHUB_TOKEN = values.Value(environ_name="GITHUB_TOKEN")
2322
RELEASE_VERSION = values.Value(environ_name="RELEASE_VERSION")
2423
SENTRY_DSN = values.SecretValue(environ_name="SENTRY_DSN")
2524
SITE_NAME = "localhost"

0 commit comments

Comments
 (0)