Skip to content
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

Improved NestBot /events command #657

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions backend/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ index-data:
load-data:
@echo "Loading Nest data"
@CMD="poetry run python manage.py load_data" $(MAKE) exec-backend-command
@CMD="poetry run python manage.py add_events_data" $(MAKE) exec-backend-command

merge-migrations:
@CMD="poetry run python manage.py makemigrations --merge" $(MAKE) exec-backend-command
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
from apps.owasp.constants import OWASP_ORGANIZATION_NAME
from apps.owasp.models.chapter import Chapter
from apps.owasp.models.committee import Committee
from apps.owasp.models.event import Event
from apps.owasp.models.project import Project

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -48,7 +47,6 @@ def handle(self, *_args, **options):

chapters = []
committees = []
events = []
projects = []

offset = options["offset"]
Expand Down Expand Up @@ -82,17 +80,12 @@ def handle(self, *_args, **options):
elif entity_key.startswith("www-project-"):
projects.append(Project.update_data(gh_repository, repository, save=False))

# OWASP events.
elif entity_key.startswith("www-event-"):
events.append(Event.update_data(gh_repository, repository, save=False))

# OWASP committees.
elif entity_key.startswith("www-committee-"):
committees.append(Committee.update_data(gh_repository, repository, save=False))

Chapter.bulk_save(chapters)
Committee.bulk_save(committees)
Event.bulk_save(events)
Project.bulk_save(projects)

# Check repository counts.
Expand Down
11 changes: 11 additions & 0 deletions backend/apps/owasp/graphql/nodes/event.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""OWASP event GraphQL node."""

from apps.common.graphql.nodes import BaseNode
from apps.owasp.models.event import Event


class EventNode(BaseNode):
"""Event node."""

class Meta:
model = Event
3 changes: 2 additions & 1 deletion backend/apps/owasp/graphql/queries/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"""OWASP GraphQL queries."""

from apps.owasp.graphql.queries.event import EventQuery
from apps.owasp.graphql.queries.project import ProjectQuery


class OwaspQuery(ProjectQuery):
class OwaspQuery(ProjectQuery, EventQuery):
"""OWASP queries."""
17 changes: 17 additions & 0 deletions backend/apps/owasp/graphql/queries/event.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""OWASP event GraphQL queries."""

import graphene

from apps.common.graphql.queries import BaseQuery
from apps.owasp.graphql.nodes.event import EventNode
from apps.owasp.models.event import Event


class EventQuery(BaseQuery):
"""Event queries."""

events = graphene.List(EventNode)

def resolve_events(root, info):
"""Resolve all events."""
return Event.objects.all()
55 changes: 55 additions & 0 deletions backend/apps/owasp/management/commands/add_events_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""A command to add events data."""

import yaml
from django.core.management.base import BaseCommand
from django.utils.text import slugify

from apps.github.utils import get_repository_file_content, normalize_url
from apps.owasp.models.event import Event


class Command(BaseCommand):
help = "Import events from the provided YAML file"

def handle(self, *args, **kwargs):
url = "https://raw.githubusercontent.com/OWASP/owasp.github.io/main/_data/events.yml"
yaml_content = get_repository_file_content(url)
data = yaml.safe_load(yaml_content)

for category in data:
category_name = category.get("category", "")
category_description = category.get("description", "")

for event_data in category["events"]:
event_name_slug = slugify(event_data.get("name", ""))
key = f"www-event-{event_name_slug}"

fields = {
"key": key,
"name": event_data.get("name", ""),
"url": normalize_url(event_data.get("url", "")) or "",
"category": category_name,
"dates": event_data.get("dates", ""),
"start_date": event_data.get("start-date", None),
"optional_text": event_data.get("optional-text", ""),
"category_description": category_description,
}

try:
event = Event.objects.get(name=fields["name"])
# Update existing event
for key, value in fields.items():
setattr(event, key, value)
event.save()
self.stdout.write(
self.style.SUCCESS(f"Successfully updated event: {event.name}")
)
except Event.DoesNotExist:
# Create new event
event = Event(**fields)
event.save()
self.stdout.write(
self.style.SUCCESS(f"Successfully created event: {event.name}")
)

self.stdout.write(self.style.SUCCESS("Finished importing events"))
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Generated by Django 5.1.5 on 2025-02-04 18:52

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("owasp", "0014_project_custom_tags"),
]

operations = [
migrations.AddField(
model_name="event",
name="category",
field=models.CharField(
blank=True, default="", max_length=100, verbose_name="Category"
),
),
migrations.AddField(
model_name="event",
name="category_description",
field=models.TextField(blank=True, default="", verbose_name="Category Description"),
),
migrations.AddField(
model_name="event",
name="dates",
field=models.CharField(blank=True, default="", max_length=100, verbose_name="Dates"),
),
migrations.AddField(
model_name="event",
name="optional_text",
field=models.TextField(blank=True, default="", verbose_name="Additional Text"),
),
migrations.AddField(
model_name="event",
name="start_date",
field=models.DateField(blank=True, null=True, verbose_name="Start Date"),
),
]
7 changes: 7 additions & 0 deletions backend/apps/owasp/models/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ class Meta:

level = models.CharField(verbose_name="Level", max_length=5, default="", blank=True)
url = models.URLField(verbose_name="URL", default="", blank=True)
category = models.CharField(verbose_name="Category", max_length=100, default="", blank=True)
dates = models.CharField(verbose_name="Dates", max_length=100, default="", blank=True)
start_date = models.DateField(verbose_name="Start Date", null=True, blank=True)
optional_text = models.TextField(verbose_name="Additional Text", default="", blank=True)
category_description = models.TextField(
verbose_name="Category Description", default="", blank=True
)

owasp_repository = models.ForeignKey(
"github.Repository", on_delete=models.SET_NULL, blank=True, null=True
Expand Down
57 changes: 52 additions & 5 deletions backend/apps/slack/commands/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

from django.conf import settings

from apps.common.constants import NL
from apps.common.constants import NL, OWASP_WEBSITE_URL
from apps.slack.apps import SlackConfig
from apps.slack.blocks import markdown
from apps.slack.utils import get_events_data

COMMAND = "/events"

Expand All @@ -16,12 +17,58 @@ def events_handler(ack, command, client):
if not settings.SLACK_COMMANDS_ENABLED:
return

blocks = [
markdown(f"Please visit <https://owasp.org/events/|OWASP events> page{NL}"),
]
events_data = get_events_data()

valid_events = [event for event in events_data if event.get("startDate")]
sorted_events = sorted(valid_events, key=lambda x: x["startDate"])

categorized_events = {}
for event in sorted_events:
category = event.get("category") or "Other"
if category not in categorized_events:
categorized_events[category] = {
"description": event.get("categoryDescription", ""),
"events": [],
}
categorized_events[category]["events"].append(event)

blocks = []
blocks.append(markdown("*Upcoming OWASP Events:*"))
blocks.append({"type": "divider"})

for category, category_data in categorized_events.items():
blocks.append(markdown(f"*{category} Events:*{NL}{category_data['description']}{NL}"))

for idx, event in enumerate(category_data["events"], 1):
if event.get("url"):
block_text = f"*{idx}. <{event['url']}|{event['name']}>*{NL}"
else:
block_text = f"*{idx}. {event['name']}*{NL}"

if event.get("startDate"):
block_text += f" Start Date: {event['startDate']}{NL}"

if event.get("dates"):
block_text += f" Duration: {event['dates']}{NL}"

if event.get("optionalText"):
block_text += f"_{event['optionalText']}_{NL}"

blocks.append(markdown(block_text))

blocks.append({"type": "divider"})

blocks.append(
markdown(
f"🔍 For more information about upcoming events, "
f"please visit <{OWASP_WEBSITE_URL}/events/|OWASP Events>{NL}"
)
)

conversation = client.conversations_open(users=command["user_id"])
client.chat_postMessage(channel=conversation["channel"]["id"], blocks=blocks)
client.chat_postMessage(
channel=conversation["channel"]["id"], text="Upcoming OWASP Events", blocks=blocks
)


if SlackConfig.app:
Expand Down
28 changes: 28 additions & 0 deletions backend/apps/slack/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from html import escape as escape_html
from urllib.parse import urljoin

import graphene
import requests
import yaml
from lxml import html
Expand Down Expand Up @@ -80,3 +81,30 @@ def get_staff_data(timeout=30):
)
except (RequestException, yaml.scanner.ScannerError):
logger.exception("Unable to parse OWASP staff data file", extra={"file_path": file_path})


def get_events_data():
"""Get raw events data via GraphQL."""
from apps.owasp.graphql.queries.event import EventQuery

query = """
query {
events {
key
name
category
dates
startDate
url
optionalText
description
categoryDescription
}
}
"""
try:
result = graphene.Schema(query=EventQuery).execute(query)
return result.data["events"]
except Exception as e:
logger.exception("Failed to fetch events data via GraphQL", extra={"error": str(e)})
return None
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
Chapter,
Command,
Committee,
Event,
Project,
Repository,
)
Expand Down Expand Up @@ -52,22 +51,22 @@ def mock_gh_repo():
(
"www-project-test",
0,
{"project": 1, "chapter": 0, "committee": 0, "event": 0},
{"project": 1, "chapter": 0, "committee": 0},
),
(
"www-chapter-test",
0,
{"project": 0, "chapter": 1, "committee": 0, "event": 0},
{"project": 0, "chapter": 1, "committee": 0},
),
(
"www-committee-test",
0,
{"project": 0, "chapter": 0, "committee": 1, "event": 0},
{"project": 0, "chapter": 0, "committee": 1},
),
(
"www-event-test",
0,
{"project": 0, "chapter": 0, "committee": 0, "event": 1},
{"project": 0, "chapter": 0, "committee": 0},
),
(None, 0, {"project": 1, "chapter": 1, "committee": 1, "event": 1}),
(None, 1, {"project": 0, "chapter": 1, "committee": 1, "event": 1}),
Expand Down Expand Up @@ -99,7 +98,6 @@ def create_mock_repo(name):
create_mock_repo("www-project-test"),
create_mock_repo("www-chapter-test"),
create_mock_repo("www-committee-test"),
create_mock_repo("www-event-test"),
create_mock_repo("www-other-test"),
]

Expand Down Expand Up @@ -131,27 +129,22 @@ def __getitem__(self, index):
mock.patch.object(Project, "bulk_save") as mock_project_bulk_save,
mock.patch.object(Chapter, "bulk_save") as mock_chapter_bulk_save,
mock.patch.object(Committee, "bulk_save") as mock_committee_bulk_save,
mock.patch.object(Event, "bulk_save") as mock_event_bulk_save,
mock.patch.object(Project, "update_data") as mock_project_update,
mock.patch.object(Chapter, "update_data") as mock_chapter_update,
mock.patch.object(Committee, "update_data") as mock_committee_update,
mock.patch.object(Event, "update_data") as mock_event_update,
mock.patch.object(Project, "objects") as mock_project_objects,
mock.patch.object(Chapter, "objects") as mock_chapter_objects,
mock.patch.object(Committee, "objects") as mock_committee_objects,
mock.patch.object(Event, "objects") as mock_event_objects,
mock.patch.object(Repository, "objects") as mock_repository_objects,
mock.patch("builtins.print") as mock_print,
):
mock_project_update.return_value = mock_repository
mock_chapter_update.return_value = mock_repository
mock_committee_update.return_value = mock_repository
mock_event_update.return_value = mock_repository

mock_project_objects.all.return_value = []
mock_chapter_objects.all.return_value = []
mock_committee_objects.all.return_value = []
mock_event_objects.all.return_value = []
mock_repository_objects.filter.return_value.count.return_value = 1

command.handle(repository=repository_name, offset=offset)
Expand All @@ -173,12 +166,9 @@ def __getitem__(self, index):
assert mock_chapter_update.call_count == expected_calls["chapter"]
elif repository_name.startswith("www-committee-"):
assert mock_committee_update.call_count == expected_calls["committee"]
elif repository_name.startswith("www-event-"):
assert mock_event_update.call_count == expected_calls["event"]
else:
assert mock_print.call_count > 0

mock_project_bulk_save.assert_called_once()
mock_chapter_bulk_save.assert_called_once()
mock_committee_bulk_save.assert_called_once()
mock_event_bulk_save.assert_called_once()
Loading