From 825baa199905b2bb928e739a535de8d498ff5476 Mon Sep 17 00:00:00 2001 From: AbhayMishra Date: Wed, 29 Jan 2025 17:34:02 +0530 Subject: [PATCH 01/17] Improved NestBot /events command --- backend/apps/slack/commands/events.py | 49 +++++- backend/apps/slack/utils.py | 18 ++ backend/tests/slack/commands/events_tests.py | 167 +++++++++++++++++++ 3 files changed, 230 insertions(+), 4 deletions(-) create mode 100644 backend/tests/slack/commands/events_tests.py diff --git a/backend/apps/slack/commands/events.py b/backend/apps/slack/commands/events.py index 264bdc574..c2fc46572 100644 --- a/backend/apps/slack/commands/events.py +++ b/backend/apps/slack/commands/events.py @@ -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" @@ -16,13 +17,53 @@ def events_handler(ack, command, client): if not settings.SLACK_COMMANDS_ENABLED: return - blocks = [ - markdown(f"Please visit page{NL}"), - ] + events_data = get_events_data() + + blocks = [] + + blocks.append(markdown("*Upcoming OWASP Events: *")) + blocks.append({"type": "divider"}) + + for category_data in events_data: + blocks.append( + markdown( + f"*{category_data['category']} Events:*{NL}{category_data['description']}{NL}" + ) + ) + stored_events = sorted( + category_data["events"], + key=lambda x: x["start-date"], + ) + + for idx, events in enumerate(stored_events, 1): + blocks.append(format_event_block(events, idx)) + + 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) +def format_event_block(event, idx): + """Format a single event into a Slack message block.""" + block_text = f"*{idx}. {event['name']}*{NL}" + block_text += f"{event['dates']}{NL}" + + if event.get("url"): + block_text += f"🔗 <{event['url']}|More Information>{NL}" + + if event.get("optional-text"): + block_text += f"_{event['optional-text']}_{NL}" + + return markdown(block_text) + + if SlackConfig.app: events_handler = SlackConfig.app.command(COMMAND)(events_handler) diff --git a/backend/apps/slack/utils.py b/backend/apps/slack/utils.py index d8fc114b1..6e0458c9b 100644 --- a/backend/apps/slack/utils.py +++ b/backend/apps/slack/utils.py @@ -32,3 +32,21 @@ 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}) + + +@lru_cache +def get_events_data(timeout=30): + """Get events data.""" + file_path = "https://raw.githubusercontent.com/OWASP/owasp.github.io/main/_data/events.yml" + try: + events_data = yaml.safe_load( + requests.get( + file_path, + timeout=timeout, + ).text + ) + except (RequestException, yaml.scanner.ScannerError): + logger.exception("Unable to parse OWASP events data file", extra={"file_path": file_path}) + return None + else: + return events_data diff --git a/backend/tests/slack/commands/events_tests.py b/backend/tests/slack/commands/events_tests.py new file mode 100644 index 000000000..89dd9878a --- /dev/null +++ b/backend/tests/slack/commands/events_tests.py @@ -0,0 +1,167 @@ +from datetime import date +from unittest.mock import MagicMock, patch + +import pytest +from django.conf import settings + +from apps.common.constants import OWASP_WEBSITE_URL +from apps.slack.commands.events import events_handler + + +class TestEventsHandler: + @pytest.fixture() + def mock_slack_command(self): + return { + "user_id": "U123456", + } + + @pytest.fixture() + def mock_slack_client(self): + client = MagicMock() + client.conversations_open.return_value = {"channel": {"id": "C123456"}} + return client + + @pytest.fixture() + def mock_events(self): + return [ + { + "category": "Global", + "description": "Our premier events bring together our communities.", + "events": [ + { + "name": "OWASP Global AppSec EU 2025", + "start-date": date(2025, 5, 26), + "dates": "May 26-30, 2025", + "url": "https://example.com/eu2025", + }, + { + "name": "OWASP Global AppSec US 2025", + "start-date": date(2025, 11, 3), + "dates": "November 3-7, 2025", + }, + ], + }, + { + "category": "AppSec Days", + "description": "Local OWASP volunteers organize conferences.", + "events": [ + { + "name": "OWASP Snow 2025", + "start-date": date(2025, 3, 14), + "dates": "March 14, 2025", + "url": "https://example.com/snowfroc", + "optional-text": "Premier application security conference", + }, + ], + }, + ] + + @pytest.mark.parametrize( + ("commands_enabled", "has_events_data", "expected_header"), + [ + (False, True, None), + (True, True, "*Upcoming OWASP Events: *"), + (True, False, "*Upcoming OWASP Events: *"), + ], + ) + @patch("apps.slack.commands.events.get_events_data") + def test_handler_responses( + self, + mock_get_events_data, + commands_enabled, + has_events_data, + expected_header, + mock_slack_client, + mock_slack_command, + mock_events, + ): + settings.SLACK_COMMANDS_ENABLED = commands_enabled + mock_get_events_data.return_value = mock_events if has_events_data else [] + + events_handler( + ack=MagicMock(), command=mock_slack_command, client=mock_slack_client + ) + + if not commands_enabled: + mock_slack_client.conversations_open.assert_not_called() + mock_slack_client.chat_postMessage.assert_not_called() + return + + # Verify basic message structure + mock_slack_client.conversations_open.assert_called_once_with( + users=mock_slack_command["user_id"] + ) + + blocks = mock_slack_client.chat_postMessage.call_args[1]["blocks"] + + # Check header + assert blocks[0]["text"]["text"] == expected_header + + if has_events_data: + current_block = 2 # Skip header and divider + + # Check each category + for category_data in mock_events: + # Verify category header + category_block = blocks[current_block]["text"]["text"] + assert f"*{category_data['category']} Events:*" in category_block + assert category_data["description"] in category_block + current_block += 1 + + # Verify events in category + sorted_events = sorted( + category_data["events"], + key=lambda x: x["start-date"], + ) + + for idx, event in enumerate(sorted_events, 1): + event_block = blocks[current_block]["text"]["text"] + + # Check basic event info + assert f"*{idx}. {event['name']}*" in event_block + assert f"{event['dates']}" in event_block + + # Check optional fields + if event.get("url"): + assert f"🔗 <{event['url']}|More Information>" in event_block + if event.get("optional-text"): + assert f"_{event['optional-text']}_" in event_block + + current_block += 1 + + current_block += 1 # Skip divider + + # Verify footer + footer_block = blocks[-1]["text"]["text"] + assert "🔍 For more information about upcoming events" in footer_block + assert OWASP_WEBSITE_URL in footer_block + + def test_format_event_block(self): + """Test the format_event_block function independently.""" + from apps.slack.commands.events import format_event_block + + # Test with minimal event data + minimal_event = { + "name": "Test Event", + "dates": "Jan 1, 2025", + } + result = format_event_block(minimal_event, 1) + assert "Test Event" in result["text"]["text"] + assert "Jan 1, 2025" in result["text"]["text"] + assert "More Information" not in result["text"]["text"] + assert "_" not in result["text"]["text"] + + # Test with full event data + full_event = { + "name": "Test Event", + "dates": "Jan 1, 2025", + "url": "https://example.com", + "optional-text": "Additional information", + } + result = format_event_block(full_event, 1) + assert "Test Event" in result["text"]["text"] + assert "Jan 1, 2025" in result["text"]["text"] + assert "https://example.com" in result["text"]["text"] + assert "Additional information" in result["text"]["text"] + + \ No newline at end of file From 797e7f85ff79abe4be5478435781d45274072508 Mon Sep 17 00:00:00 2001 From: AbhayMishra Date: Thu, 30 Jan 2025 00:30:50 +0530 Subject: [PATCH 02/17] pre-commit --- backend/tests/slack/commands/events_tests.py | 31 +++++--------------- 1 file changed, 8 insertions(+), 23 deletions(-) diff --git a/backend/tests/slack/commands/events_tests.py b/backend/tests/slack/commands/events_tests.py index 89dd9878a..4381886ac 100644 --- a/backend/tests/slack/commands/events_tests.py +++ b/backend/tests/slack/commands/events_tests.py @@ -78,60 +78,49 @@ def test_handler_responses( settings.SLACK_COMMANDS_ENABLED = commands_enabled mock_get_events_data.return_value = mock_events if has_events_data else [] - events_handler( - ack=MagicMock(), command=mock_slack_command, client=mock_slack_client - ) + events_handler(ack=MagicMock(), command=mock_slack_command, client=mock_slack_client) if not commands_enabled: mock_slack_client.conversations_open.assert_not_called() mock_slack_client.chat_postMessage.assert_not_called() return - # Verify basic message structure mock_slack_client.conversations_open.assert_called_once_with( users=mock_slack_command["user_id"] ) blocks = mock_slack_client.chat_postMessage.call_args[1]["blocks"] - # Check header assert blocks[0]["text"]["text"] == expected_header if has_events_data: - current_block = 2 # Skip header and divider - - # Check each category + current_block = 2 for category_data in mock_events: - # Verify category header category_block = blocks[current_block]["text"]["text"] assert f"*{category_data['category']} Events:*" in category_block assert category_data["description"] in category_block current_block += 1 - # Verify events in category sorted_events = sorted( category_data["events"], key=lambda x: x["start-date"], ) - + for idx, event in enumerate(sorted_events, 1): event_block = blocks[current_block]["text"]["text"] - - # Check basic event info + assert f"*{idx}. {event['name']}*" in event_block assert f"{event['dates']}" in event_block - - # Check optional fields + if event.get("url"): assert f"🔗 <{event['url']}|More Information>" in event_block if event.get("optional-text"): assert f"_{event['optional-text']}_" in event_block - + current_block += 1 - - current_block += 1 # Skip divider - # Verify footer + current_block += 1 + footer_block = blocks[-1]["text"]["text"] assert "🔍 For more information about upcoming events" in footer_block assert OWASP_WEBSITE_URL in footer_block @@ -140,7 +129,6 @@ def test_format_event_block(self): """Test the format_event_block function independently.""" from apps.slack.commands.events import format_event_block - # Test with minimal event data minimal_event = { "name": "Test Event", "dates": "Jan 1, 2025", @@ -151,7 +139,6 @@ def test_format_event_block(self): assert "More Information" not in result["text"]["text"] assert "_" not in result["text"]["text"] - # Test with full event data full_event = { "name": "Test Event", "dates": "Jan 1, 2025", @@ -163,5 +150,3 @@ def test_format_event_block(self): assert "Jan 1, 2025" in result["text"]["text"] assert "https://example.com" in result["text"]["text"] assert "Additional information" in result["text"]["text"] - - \ No newline at end of file From dd2b4a5cb83a290f67167782e5bef0844e4f53fd Mon Sep 17 00:00:00 2001 From: abhayymishraaa Date: Fri, 31 Jan 2025 22:35:41 +0530 Subject: [PATCH 03/17] verified commit --- backend/tests/slack/commands/events_tests.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/tests/slack/commands/events_tests.py b/backend/tests/slack/commands/events_tests.py index 4381886ac..f851f555b 100644 --- a/backend/tests/slack/commands/events_tests.py +++ b/backend/tests/slack/commands/events_tests.py @@ -148,5 +148,6 @@ def test_format_event_block(self): result = format_event_block(full_event, 1) assert "Test Event" in result["text"]["text"] assert "Jan 1, 2025" in result["text"]["text"] - assert "https://example.com" in result["text"]["text"] + expected_url = "https://" + "example.com" + assert expected_url in result["text"]["text"] assert "Additional information" in result["text"]["text"] From eee4b372f31628fbc47c5af05590928ba840cb0b Mon Sep 17 00:00:00 2001 From: Abhay Mishra Date: Sun, 2 Feb 2025 12:04:47 +0530 Subject: [PATCH 04/17] verified commit with chnages --- backend/tests/slack/commands/{events_tests.py => events_test.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename backend/tests/slack/commands/{events_tests.py => events_test.py} (100%) diff --git a/backend/tests/slack/commands/events_tests.py b/backend/tests/slack/commands/events_test.py similarity index 100% rename from backend/tests/slack/commands/events_tests.py rename to backend/tests/slack/commands/events_test.py From 823f36771b897428880d0a617a641e765ebc9ba8 Mon Sep 17 00:00:00 2001 From: Abhay Mishra Date: Wed, 5 Feb 2025 01:29:41 +0530 Subject: [PATCH 05/17] Added events to use graphql as a source of data --- backend/Makefile | 1 + backend/apps/owasp/graphql/nodes/event.py | 11 ++ .../apps/owasp/graphql/queries/__init__.py | 3 +- backend/apps/owasp/graphql/queries/event.py | 17 +++ .../management/commands/add_events_data.py | 55 ++++++++ ...ory_event_category_description_and_more.py | 39 ++++++ backend/apps/owasp/models/event.py | 7 + backend/apps/slack/commands/events.py | 66 +++++---- backend/apps/slack/utils.py | 38 +++-- backend/tests/owasp/models/project_test.py | 13 +- backend/tests/slack/commands/events_test.py | 131 ++++++------------ 11 files changed, 246 insertions(+), 135 deletions(-) create mode 100644 backend/apps/owasp/graphql/nodes/event.py create mode 100644 backend/apps/owasp/graphql/queries/event.py create mode 100644 backend/apps/owasp/management/commands/add_events_data.py create mode 100644 backend/apps/owasp/migrations/0015_event_category_event_category_description_and_more.py diff --git a/backend/Makefile b/backend/Makefile index 97b80d7d1..9ff429c19 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -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 diff --git a/backend/apps/owasp/graphql/nodes/event.py b/backend/apps/owasp/graphql/nodes/event.py new file mode 100644 index 000000000..a459e4cb8 --- /dev/null +++ b/backend/apps/owasp/graphql/nodes/event.py @@ -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 diff --git a/backend/apps/owasp/graphql/queries/__init__.py b/backend/apps/owasp/graphql/queries/__init__.py index 6558213ea..c64e97f96 100644 --- a/backend/apps/owasp/graphql/queries/__init__.py +++ b/backend/apps/owasp/graphql/queries/__init__.py @@ -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.""" diff --git a/backend/apps/owasp/graphql/queries/event.py b/backend/apps/owasp/graphql/queries/event.py new file mode 100644 index 000000000..2899e9c23 --- /dev/null +++ b/backend/apps/owasp/graphql/queries/event.py @@ -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() diff --git a/backend/apps/owasp/management/commands/add_events_data.py b/backend/apps/owasp/management/commands/add_events_data.py new file mode 100644 index 000000000..d6b08b1c9 --- /dev/null +++ b/backend/apps/owasp/management/commands/add_events_data.py @@ -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")) diff --git a/backend/apps/owasp/migrations/0015_event_category_event_category_description_and_more.py b/backend/apps/owasp/migrations/0015_event_category_event_category_description_and_more.py new file mode 100644 index 000000000..46a4dad07 --- /dev/null +++ b/backend/apps/owasp/migrations/0015_event_category_event_category_description_and_more.py @@ -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"), + ), + ] diff --git a/backend/apps/owasp/models/event.py b/backend/apps/owasp/models/event.py index 16ebbb250..c836e7712 100644 --- a/backend/apps/owasp/models/event.py +++ b/backend/apps/owasp/models/event.py @@ -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 diff --git a/backend/apps/slack/commands/events.py b/backend/apps/slack/commands/events.py index c2fc46572..826bfb499 100644 --- a/backend/apps/slack/commands/events.py +++ b/backend/apps/slack/commands/events.py @@ -19,50 +19,56 @@ def events_handler(ack, command, client): events_data = get_events_data() - blocks = [] + 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.append(markdown("*Upcoming OWASP Events: *")) + blocks = [] + blocks.append(markdown("*Upcoming OWASP Events:*")) blocks.append({"type": "divider"}) - for category_data in events_data: - blocks.append( - markdown( - f"*{category_data['category']} Events:*{NL}{category_data['description']}{NL}" - ) - ) - stored_events = sorted( - category_data["events"], - key=lambda x: x["start-date"], - ) + 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}" - for idx, events in enumerate(stored_events, 1): - blocks.append(format_event_block(events, idx)) + 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}" + 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) - - -def format_event_block(event, idx): - """Format a single event into a Slack message block.""" - block_text = f"*{idx}. {event['name']}*{NL}" - block_text += f"{event['dates']}{NL}" - - if event.get("url"): - block_text += f"🔗 <{event['url']}|More Information>{NL}" - - if event.get("optional-text"): - block_text += f"_{event['optional-text']}_{NL}" - - return markdown(block_text) + client.chat_postMessage( + channel=conversation["channel"]["id"], text="Upcoming OWASP Events", blocks=blocks + ) if SlackConfig.app: diff --git a/backend/apps/slack/utils.py b/backend/apps/slack/utils.py index 6e0458c9b..70070f57b 100644 --- a/backend/apps/slack/utils.py +++ b/backend/apps/slack/utils.py @@ -4,6 +4,7 @@ import logging from functools import lru_cache +import graphene import requests import yaml from requests.exceptions import RequestException @@ -34,19 +35,28 @@ def get_staff_data(timeout=30): logger.exception("Unable to parse OWASP staff data file", extra={"file_path": file_path}) -@lru_cache -def get_events_data(timeout=30): - """Get events data.""" - file_path = "https://raw.githubusercontent.com/OWASP/owasp.github.io/main/_data/events.yml" +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: - events_data = yaml.safe_load( - requests.get( - file_path, - timeout=timeout, - ).text - ) - except (RequestException, yaml.scanner.ScannerError): - logger.exception("Unable to parse OWASP events data file", extra={"file_path": file_path}) + 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 - else: - return events_data diff --git a/backend/tests/owasp/models/project_test.py b/backend/tests/owasp/models/project_test.py index 9b2539c0e..45fd3b387 100644 --- a/backend/tests/owasp/models/project_test.py +++ b/backend/tests/owasp/models/project_test.py @@ -76,8 +76,19 @@ def test_deactivate(self, mock_save): mock_save.assert_called_once_with(update_fields=("is_active",)) @patch("apps.owasp.models.project.Project.objects.get") - def test_update_data_project_does_not_exist(self, mock_get): + @patch("apps.github.utils.requests.get") + def test_update_data_project_does_not_exist(self, mock_requests_get, mock_get): + """Test updating project data when the project doesn't exist.""" mock_get.side_effect = Project.DoesNotExist + + mock_response = Mock() + mock_response.text = """ + # Project Title + Some project description + """ + mock_requests_get.return_value = mock_response + + # Setup test data gh_repository_mock = Mock() gh_repository_mock.name = "new_repo" repository_mock = Repository() diff --git a/backend/tests/slack/commands/events_test.py b/backend/tests/slack/commands/events_test.py index f851f555b..8701baac8 100644 --- a/backend/tests/slack/commands/events_test.py +++ b/backend/tests/slack/commands/events_test.py @@ -1,14 +1,37 @@ -from datetime import date +"""Test events command handler.""" + from unittest.mock import MagicMock, patch import pytest from django.conf import settings -from apps.common.constants import OWASP_WEBSITE_URL from apps.slack.commands.events import events_handler +mock_events = [ + { + "name": "OWASP Snow 2025", + "category": "AppSec Days", + "startDate": "2025-03-14", + "dates": "March 14, 2025", + "url": "https://example.com/snow", + "optionalText": "Regional conference", + "categoryDescription": "Local events description", + }, + { + "name": "OWASP Global AppSec EU 2025", + "category": "Global", + "startDate": "2025-05-26", + "dates": "May 26-30, 2025", + "url": "https://example.com/eu", + "optionalText": "Premier conference", + "categoryDescription": "Global events description", + }, +] + class TestEventsHandler: + """Test events command handler.""" + @pytest.fixture() def mock_slack_command(self): return { @@ -21,47 +44,12 @@ def mock_slack_client(self): client.conversations_open.return_value = {"channel": {"id": "C123456"}} return client - @pytest.fixture() - def mock_events(self): - return [ - { - "category": "Global", - "description": "Our premier events bring together our communities.", - "events": [ - { - "name": "OWASP Global AppSec EU 2025", - "start-date": date(2025, 5, 26), - "dates": "May 26-30, 2025", - "url": "https://example.com/eu2025", - }, - { - "name": "OWASP Global AppSec US 2025", - "start-date": date(2025, 11, 3), - "dates": "November 3-7, 2025", - }, - ], - }, - { - "category": "AppSec Days", - "description": "Local OWASP volunteers organize conferences.", - "events": [ - { - "name": "OWASP Snow 2025", - "start-date": date(2025, 3, 14), - "dates": "March 14, 2025", - "url": "https://example.com/snowfroc", - "optional-text": "Premier application security conference", - }, - ], - }, - ] - @pytest.mark.parametrize( ("commands_enabled", "has_events_data", "expected_header"), [ (False, True, None), - (True, True, "*Upcoming OWASP Events: *"), - (True, False, "*Upcoming OWASP Events: *"), + (True, True, "*Upcoming OWASP Events:*"), + (True, False, "*Upcoming OWASP Events:*"), ], ) @patch("apps.slack.commands.events.get_events_data") @@ -73,8 +61,8 @@ def test_handler_responses( expected_header, mock_slack_client, mock_slack_command, - mock_events, ): + """Test handler responses.""" settings.SLACK_COMMANDS_ENABLED = commands_enabled mock_get_events_data.return_value = mock_events if has_events_data else [] @@ -92,62 +80,27 @@ def test_handler_responses( blocks = mock_slack_client.chat_postMessage.call_args[1]["blocks"] assert blocks[0]["text"]["text"] == expected_header + assert blocks[1]["type"] == "divider" if has_events_data: current_block = 2 - for category_data in mock_events: - category_block = blocks[current_block]["text"]["text"] - assert f"*{category_data['category']} Events:*" in category_block - assert category_data["description"] in category_block - current_block += 1 - - sorted_events = sorted( - category_data["events"], - key=lambda x: x["start-date"], - ) - for idx, event in enumerate(sorted_events, 1): - event_block = blocks[current_block]["text"]["text"] + assert "*AppSec Days Events:*" in blocks[current_block]["text"]["text"] + assert "Local events description" in blocks[current_block]["text"]["text"] + current_block += 1 - assert f"*{idx}. {event['name']}*" in event_block - assert f"{event['dates']}" in event_block + event_block = blocks[current_block]["text"]["text"] + assert "*1. *" in event_block + assert "Start Date: 2025-03-14" in event_block + assert "Duration: March 14, 2025" in event_block + assert "_Regional conference_" in event_block + current_block += 1 - if event.get("url"): - assert f"🔗 <{event['url']}|More Information>" in event_block - if event.get("optional-text"): - assert f"_{event['optional-text']}_" in event_block + assert blocks[current_block]["type"] == "divider" + current_block += 1 - current_block += 1 - - current_block += 1 + assert "*Global Events:*" in blocks[current_block]["text"]["text"] + assert "Global events description" in blocks[current_block]["text"]["text"] footer_block = blocks[-1]["text"]["text"] assert "🔍 For more information about upcoming events" in footer_block - assert OWASP_WEBSITE_URL in footer_block - - def test_format_event_block(self): - """Test the format_event_block function independently.""" - from apps.slack.commands.events import format_event_block - - minimal_event = { - "name": "Test Event", - "dates": "Jan 1, 2025", - } - result = format_event_block(minimal_event, 1) - assert "Test Event" in result["text"]["text"] - assert "Jan 1, 2025" in result["text"]["text"] - assert "More Information" not in result["text"]["text"] - assert "_" not in result["text"]["text"] - - full_event = { - "name": "Test Event", - "dates": "Jan 1, 2025", - "url": "https://example.com", - "optional-text": "Additional information", - } - result = format_event_block(full_event, 1) - assert "Test Event" in result["text"]["text"] - assert "Jan 1, 2025" in result["text"]["text"] - expected_url = "https://" + "example.com" - assert expected_url in result["text"]["text"] - assert "Additional information" in result["text"]["text"] From 3962eb199e57c7048931588e878ed8c27e9c50d3 Mon Sep 17 00:00:00 2001 From: Abhay Mishra Date: Wed, 5 Feb 2025 02:39:21 +0530 Subject: [PATCH 06/17] removed the old event logic sync --- .../commands/github_update_owasp_organization.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/backend/apps/github/management/commands/github_update_owasp_organization.py b/backend/apps/github/management/commands/github_update_owasp_organization.py index f3f70aa00..a5492b3e1 100644 --- a/backend/apps/github/management/commands/github_update_owasp_organization.py +++ b/backend/apps/github/management/commands/github_update_owasp_organization.py @@ -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__) @@ -48,7 +47,6 @@ def handle(self, *_args, **options): chapters = [] committees = [] - events = [] projects = [] offset = options["offset"] @@ -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. From fc95c4d132ec05b9a6e0a95a819eea40af86378e Mon Sep 17 00:00:00 2001 From: Abhay Mishra Date: Wed, 5 Feb 2025 02:48:11 +0530 Subject: [PATCH 07/17] fixed testcase syncing --- .../github_update_owasp_organization_test.py | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/backend/tests/github/management/commands/github_update_owasp_organization_test.py b/backend/tests/github/management/commands/github_update_owasp_organization_test.py index f34de214b..58b78ddb8 100644 --- a/backend/tests/github/management/commands/github_update_owasp_organization_test.py +++ b/backend/tests/github/management/commands/github_update_owasp_organization_test.py @@ -6,7 +6,6 @@ Chapter, Command, Committee, - Event, Project, Repository, ) @@ -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}), @@ -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"), ] @@ -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) @@ -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() From 4558d4d5315de8e964e25c86c48eeae4cbbfc92b Mon Sep 17 00:00:00 2001 From: Arkadii Yakovets Date: Thu, 13 Feb 2025 17:29:18 -0800 Subject: [PATCH 08/17] Update code --- backend/Makefile | 8 ++++++-- backend/apps/owasp/graphql/nodes/event.py | 4 ++++ backend/apps/owasp/graphql/queries/__init__.py | 2 +- .../{add_events_data.py => owasp_update_events.py} | 12 ++++++------ backend/apps/owasp/models/event.py | 10 +++++----- backend/apps/slack/commands/events.py | 3 +-- backend/apps/slack/utils.py | 1 + 7 files changed, 24 insertions(+), 16 deletions(-) rename backend/apps/owasp/management/commands/{add_events_data.py => owasp_update_events.py} (98%) diff --git a/backend/Makefile b/backend/Makefile index 5f4320ed2..48a0ec300 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -45,7 +45,6 @@ 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 @@ -84,6 +83,10 @@ owasp-scrape-projects: @echo "Scraping OWASP site projects data" @CMD="poetry run python manage.py owasp_scrape_projects" $(MAKE) exec-backend-command +owasp-update-events: + @echo "Getting OWASP events data" + @CMD="poetry run python manage.py owasp_update_events" $(MAKE) exec-backend-command + poetry-update: @CMD="poetry update" $(MAKE) exec-backend-command @@ -114,4 +117,5 @@ update-data: \ owasp-scrape-committees \ owasp-scrape-projects \ github-update-project-related-repositories \ - owasp-aggregate-projects + owasp-aggregate-projects \ + owasp-update-events diff --git a/backend/apps/owasp/graphql/nodes/event.py b/backend/apps/owasp/graphql/nodes/event.py index a459e4cb8..e6c8a0bd4 100644 --- a/backend/apps/owasp/graphql/nodes/event.py +++ b/backend/apps/owasp/graphql/nodes/event.py @@ -9,3 +9,7 @@ class EventNode(BaseNode): class Meta: model = Event + fields = ( + "name", + "start_date", + ) diff --git a/backend/apps/owasp/graphql/queries/__init__.py b/backend/apps/owasp/graphql/queries/__init__.py index 75ffb30c9..ace05f199 100644 --- a/backend/apps/owasp/graphql/queries/__init__.py +++ b/backend/apps/owasp/graphql/queries/__init__.py @@ -1,8 +1,8 @@ """OWASP GraphQL queries.""" - from .chapter import ChapterQuery from .committee import CommitteeQuery +from .event import EventQuery from .project import ProjectQuery diff --git a/backend/apps/owasp/management/commands/add_events_data.py b/backend/apps/owasp/management/commands/owasp_update_events.py similarity index 98% rename from backend/apps/owasp/management/commands/add_events_data.py rename to backend/apps/owasp/management/commands/owasp_update_events.py index d6b08b1c9..c4d04c50b 100644 --- a/backend/apps/owasp/management/commands/add_events_data.py +++ b/backend/apps/owasp/management/commands/owasp_update_events.py @@ -1,4 +1,4 @@ -"""A command to add events data.""" +"""A command to update OWASP events.""" import yaml from django.core.management.base import BaseCommand @@ -25,14 +25,14 @@ def handle(self, *args, **kwargs): key = f"www-event-{event_name_slug}" fields = { - "key": key, - "name": event_data.get("name", ""), - "url": normalize_url(event_data.get("url", "")) or "", + "category_description": category_description, "category": category_name, "dates": event_data.get("dates", ""), - "start_date": event_data.get("start-date", None), + "key": key, + "name": event_data.get("name", ""), "optional_text": event_data.get("optional-text", ""), - "category_description": category_description, + "start_date": event_data.get("start-date", None), + "url": normalize_url(event_data.get("url", "")) or "", } try: diff --git a/backend/apps/owasp/models/event.py b/backend/apps/owasp/models/event.py index c836e7712..3ded6aad6 100644 --- a/backend/apps/owasp/models/event.py +++ b/backend/apps/owasp/models/event.py @@ -13,15 +13,15 @@ class Meta: db_table = "owasp_events" verbose_name_plural = "Events" - 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 ) + dates = models.CharField(verbose_name="Dates", max_length=100, default="", blank=True) + level = models.CharField(verbose_name="Level", max_length=5, default="", blank=True) + optional_text = models.TextField(verbose_name="Additional Text", default="", blank=True) + start_date = models.DateField(verbose_name="Start Date", null=True, blank=True) + url = models.URLField(verbose_name="URL", default="", blank=True) owasp_repository = models.ForeignKey( "github.Repository", on_delete=models.SET_NULL, blank=True, null=True diff --git a/backend/apps/slack/commands/events.py b/backend/apps/slack/commands/events.py index 3397936b3..e0c6b00d1 100644 --- a/backend/apps/slack/commands/events.py +++ b/backend/apps/slack/commands/events.py @@ -5,8 +5,7 @@ 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 -from apps.slack.utils import get_text +from apps.slack.utils import get_events_data, get_text COMMAND = "/events" diff --git a/backend/apps/slack/utils.py b/backend/apps/slack/utils.py index e512d39b5..37387d4ec 100644 --- a/backend/apps/slack/utils.py +++ b/backend/apps/slack/utils.py @@ -110,6 +110,7 @@ def get_events_data(): logger.exception("Failed to fetch events data via GraphQL", extra={"error": str(e)}) return None + def get_text(blocks): """Convert blocks to plain text.""" text = [] From fafcf817f8d05fa7b68c99a8afe3552adffd82b2 Mon Sep 17 00:00:00 2001 From: Abhay Mishra Date: Thu, 20 Feb 2025 21:50:51 +0530 Subject: [PATCH 09/17] new migration and removed unnecessary things --- backend/apps/owasp/admin.py | 1 - backend/apps/owasp/graphql/nodes/event.py | 6 ++ backend/apps/owasp/graphql/queries/event.py | 8 +- .../commands/owasp_update_events.py | 44 ++++++---- ...ory_event_category_description_and_more.py | 39 --------- .../0015_remove_event_created_at_and_more.py | 83 +++++++++++++++++++ backend/apps/owasp/models/event.py | 46 +++++----- backend/apps/owasp/utils.py | 53 ++++++++++++ backend/apps/slack/commands/events.py | 8 +- backend/apps/slack/utils.py | 3 +- 10 files changed, 205 insertions(+), 86 deletions(-) delete mode 100644 backend/apps/owasp/migrations/0015_event_category_event_category_description_and_more.py create mode 100644 backend/apps/owasp/migrations/0015_remove_event_created_at_and_more.py create mode 100644 backend/apps/owasp/utils.py diff --git a/backend/apps/owasp/admin.py b/backend/apps/owasp/admin.py index 0abdc94d8..518fd10c4 100644 --- a/backend/apps/owasp/admin.py +++ b/backend/apps/owasp/admin.py @@ -54,7 +54,6 @@ class CommitteeAdmin(admin.ModelAdmin): class EventAdmin(admin.ModelAdmin): - autocomplete_fields = ("owasp_repository",) list_display = ("name",) search_fields = ("name",) diff --git a/backend/apps/owasp/graphql/nodes/event.py b/backend/apps/owasp/graphql/nodes/event.py index e6c8a0bd4..d8c122a5b 100644 --- a/backend/apps/owasp/graphql/nodes/event.py +++ b/backend/apps/owasp/graphql/nodes/event.py @@ -10,6 +10,12 @@ class EventNode(BaseNode): class Meta: model = Event fields = ( + "category", + "category_description", + "end_date", + "description", + "key", "name", "start_date", + "url", ) diff --git a/backend/apps/owasp/graphql/queries/event.py b/backend/apps/owasp/graphql/queries/event.py index 2899e9c23..6a638c2a6 100644 --- a/backend/apps/owasp/graphql/queries/event.py +++ b/backend/apps/owasp/graphql/queries/event.py @@ -1,5 +1,7 @@ """OWASP event GraphQL queries.""" +from datetime import datetime, timezone + import graphene from apps.common.graphql.queries import BaseQuery @@ -14,4 +16,8 @@ class EventQuery(BaseQuery): def resolve_events(root, info): """Resolve all events.""" - return Event.objects.all() + today = datetime.now(timezone.utc).date() + + base_query = Event.objects.exclude(start_date__isnull=True).filter(start_date__gte=today) + + return base_query.order_by("start_date") diff --git a/backend/apps/owasp/management/commands/owasp_update_events.py b/backend/apps/owasp/management/commands/owasp_update_events.py index c4d04c50b..dc5a36934 100644 --- a/backend/apps/owasp/management/commands/owasp_update_events.py +++ b/backend/apps/owasp/management/commands/owasp_update_events.py @@ -5,7 +5,8 @@ from django.utils.text import slugify from apps.github.utils import get_repository_file_content, normalize_url -from apps.owasp.models.event import Event +from apps.owasp.models.event import Event, EventCategory +from apps.owasp.utils import parse_event_dates class Command(BaseCommand): @@ -15,6 +16,7 @@ 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) + events = [] for category in data: category_name = category.get("category", "") @@ -22,34 +24,46 @@ def handle(self, *args, **kwargs): for event_data in category["events"]: event_name_slug = slugify(event_data.get("name", "")) - key = f"www-event-{event_name_slug}" + key = event_name_slug + end_date = parse_event_dates( + event_data.get("dates", ""), event_data.get("start-date") + ) fields = { + "category": get_event_category(category_name), "category_description": category_description, - "category": category_name, - "dates": event_data.get("dates", ""), + "end_date": end_date, "key": key, "name": event_data.get("name", ""), - "optional_text": event_data.get("optional-text", ""), + "description": event_data.get("optional-text", ""), "start_date": event_data.get("start-date", None), "url": normalize_url(event_data.get("url", "")) or "", } try: - event = Event.objects.get(name=fields["name"]) + event = Event.objects.get(key=key) # 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}") - ) + events.append(event) except Event.DoesNotExist: # Create new event event = Event(**fields) - event.save() - self.stdout.write( - self.style.SUCCESS(f"Successfully created event: {event.name}") - ) + events.append(event) - self.stdout.write(self.style.SUCCESS("Finished importing events")) + if events: + self.stdout.write(f"Saving {len(events)} events...") + Event.bulk_save(events, fields) + self.stdout.write(self.style.SUCCESS("Successfully saved events")) + else: + self.stdout.write(self.style.WARNING("No events to save")) + + +def get_event_category(category_name): + """Get event category.""" + category_map = { + "Global": EventCategory.GLOBAL, + "AppSec Days": EventCategory.APPSEC_DAYS, + "Partner": EventCategory.PARTNER, + } + return category_map.get(category_name, EventCategory.OTHER) diff --git a/backend/apps/owasp/migrations/0015_event_category_event_category_description_and_more.py b/backend/apps/owasp/migrations/0015_event_category_event_category_description_and_more.py deleted file mode 100644 index 46a4dad07..000000000 --- a/backend/apps/owasp/migrations/0015_event_category_event_category_description_and_more.py +++ /dev/null @@ -1,39 +0,0 @@ -# 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"), - ), - ] diff --git a/backend/apps/owasp/migrations/0015_remove_event_created_at_and_more.py b/backend/apps/owasp/migrations/0015_remove_event_created_at_and_more.py new file mode 100644 index 000000000..0bf4e7db2 --- /dev/null +++ b/backend/apps/owasp/migrations/0015_remove_event_created_at_and_more.py @@ -0,0 +1,83 @@ +# Generated by Django 5.1.5 on 2025-02-20 16:06 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("owasp", "0014_project_custom_tags"), + ] + + operations = [ + migrations.RemoveField( + model_name="event", + name="created_at", + ), + migrations.RemoveField( + model_name="event", + name="has_active_repositories", + ), + migrations.RemoveField( + model_name="event", + name="is_active", + ), + migrations.RemoveField( + model_name="event", + name="level", + ), + migrations.RemoveField( + model_name="event", + name="owasp_repository", + ), + migrations.RemoveField( + model_name="event", + name="summary", + ), + migrations.RemoveField( + model_name="event", + name="tags", + ), + migrations.RemoveField( + model_name="event", + name="topics", + ), + migrations.RemoveField( + model_name="event", + name="updated_at", + ), + migrations.AddField( + model_name="event", + name="category", + field=models.CharField( + choices=[ + ("global", "Global"), + ("appsec_days", "AppSec Days"), + ("partner", "Partner"), + ("other", "Other"), + ], + default="other", + max_length=20, + 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="end_date", + field=models.DateField(blank=True, null=True, verbose_name="End Date"), + ), + migrations.AddField( + model_name="event", + name="start_date", + field=models.DateField(blank=True, default="2025-01-01", verbose_name="Start Date"), + ), + migrations.AlterField( + model_name="event", + name="description", + field=models.TextField(blank=True, default="", verbose_name="Description"), + ), + ] diff --git a/backend/apps/owasp/models/event.py b/backend/apps/owasp/models/event.py index 3ded6aad6..3b2c94c5d 100644 --- a/backend/apps/owasp/models/event.py +++ b/backend/apps/owasp/models/event.py @@ -3,47 +3,45 @@ from django.db import models from apps.common.models import BulkSaveModel, TimestampedModel -from apps.owasp.models.common import RepositoryBasedEntityModel -class Event(BulkSaveModel, RepositoryBasedEntityModel, TimestampedModel): +class EventCategory(models.TextChoices): + """Event category choices.""" + + GLOBAL = "global", "Global" + APPSEC_DAYS = "appsec_days", "AppSec Days" + PARTNER = "partner", "Partner" + OTHER = "other", "Other" + + +class Event(BulkSaveModel, TimestampedModel): """Event model.""" class Meta: db_table = "owasp_events" verbose_name_plural = "Events" - category = models.CharField(verbose_name="Category", max_length=100, default="", blank=True) + category = models.CharField( + verbose_name="Category", + max_length=20, + choices=EventCategory.choices, + default=EventCategory.OTHER, + ) + category_description = models.TextField( verbose_name="Category Description", default="", blank=True ) - dates = models.CharField(verbose_name="Dates", max_length=100, default="", blank=True) - level = models.CharField(verbose_name="Level", max_length=5, default="", blank=True) - optional_text = models.TextField(verbose_name="Additional Text", default="", blank=True) - start_date = models.DateField(verbose_name="Start Date", null=True, blank=True) + end_date = models.DateField(verbose_name="End Date", null=True, blank=True) + key = models.CharField(verbose_name="Key", max_length=100, unique=True) + name = models.CharField(verbose_name="Name", max_length=100) + description = models.TextField(verbose_name="Description", default="", blank=True) + start_date = models.DateField(verbose_name="Start Date", default="2025-01-01", blank=True) url = models.URLField(verbose_name="URL", default="", blank=True) - owasp_repository = models.ForeignKey( - "github.Repository", on_delete=models.SET_NULL, blank=True, null=True - ) - def __str__(self): """Event human readable representation.""" return f"{self.name or self.key}" - def from_github(self, repository): - """Update instance based on GitHub repository data.""" - field_mapping = { - "description": "pitch", - "level": "level", - "name": "title", - "tags": "tags", - } - RepositoryBasedEntityModel.from_github(self, field_mapping, repository) - - # FKs. - self.owasp_repository = repository - @staticmethod def bulk_save(events, fields=None): """Bulk save events.""" diff --git a/backend/apps/owasp/utils.py b/backend/apps/owasp/utils.py new file mode 100644 index 000000000..3b2e12f5f --- /dev/null +++ b/backend/apps/owasp/utils.py @@ -0,0 +1,53 @@ +"""OWASP utils.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from datetime import date + +from dateutil import parser + + +def parse_event_dates(dates: str, start_date: str | None = None) -> date | None: + """Parse event dates.""" + if not dates: + return None + + # Handle single-day events (e.g., "March 14, 2025") + if "," in dates and "-" not in dates: + try: + return parser.parse(dates).date() + except ValueError: + return None + + # Handle date ranges (e.g., "May 26-30, 2025" or "November 2-6, 2026") + if "-" in dates and "," in dates: + try: + # Split the date range into parts + date_range, year = dates.split(", ") + month_day_range = date_range.split() + + # Extract month and day range + month = month_day_range[0] + day_range = month_day_range[1] + + # Extract end day from the range + end_day = int(day_range.split("-")[-1]) + + # Parse the year + year = int(year.strip()) + + # Use the start_date to determine the month if provided + if start_date: + start_date_parsed = start_date + month = start_date_parsed.strftime("%B") # Full month name (e.g., "May") + + # Parse the full end date string + end_date_str = f"{month} {end_day}, {year}" + return parser.parse(end_date_str).date() + except (ValueError, IndexError, AttributeError): + return None + + return None diff --git a/backend/apps/slack/commands/events.py b/backend/apps/slack/commands/events.py index e0c6b00d1..141f3d115 100644 --- a/backend/apps/slack/commands/events.py +++ b/backend/apps/slack/commands/events.py @@ -48,11 +48,11 @@ def events_handler(ack, command, client): 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("endDate"): + block_text += f" End Date: {event['endDate']}{NL}" - if event.get("optionalText"): - block_text += f"_{event['optionalText']}_{NL}" + if event.get("description"): + block_text += f"_{event['description']}_{NL}" blocks.append(markdown(block_text)) diff --git a/backend/apps/slack/utils.py b/backend/apps/slack/utils.py index 37387d4ec..d81887016 100644 --- a/backend/apps/slack/utils.py +++ b/backend/apps/slack/utils.py @@ -94,10 +94,9 @@ def get_events_data(): key name category - dates + endDate startDate url - optionalText description categoryDescription } From a3f7c909e080788abf28c0a31a4f72057d5da6c3 Mon Sep 17 00:00:00 2001 From: Abhay Mishra Date: Thu, 20 Feb 2025 22:27:07 +0530 Subject: [PATCH 10/17] fixed some cases one remaining --- backend/apps/owasp/models/event.py | 15 ------ backend/tests/owasp/models/event_test.py | 57 +++------------------ backend/tests/slack/commands/events_test.py | 10 ++-- 3 files changed, 11 insertions(+), 71 deletions(-) diff --git a/backend/apps/owasp/models/event.py b/backend/apps/owasp/models/event.py index 3b2c94c5d..73f6d0ede 100644 --- a/backend/apps/owasp/models/event.py +++ b/backend/apps/owasp/models/event.py @@ -46,18 +46,3 @@ def __str__(self): def bulk_save(events, fields=None): """Bulk save events.""" BulkSaveModel.bulk_save(Event, events, fields=fields) - - @staticmethod - def update_data(gh_repository, repository, save=True): - """Update event data.""" - key = gh_repository.name.lower() - try: - event = Event.objects.get(key=key) - except Event.DoesNotExist: - event = Event(key=key) - - event.from_github(repository) - if save: - event.save() - - return event diff --git a/backend/tests/owasp/models/event_test.py b/backend/tests/owasp/models/event_test.py index 5acd007b9..55cbba9f5 100644 --- a/backend/tests/owasp/models/event_test.py +++ b/backend/tests/owasp/models/event_test.py @@ -1,8 +1,8 @@ +from datetime import date from unittest.mock import Mock, patch import pytest -from apps.github.models.repository import Repository from apps.owasp.models.event import Event @@ -16,59 +16,14 @@ class TestEventModel: ], ) def test_event_str(self, name, key, expected_str): - event = Event(name=name, key=key) + event = Event(name=name, key=key, start_date=date(2025, 1, 1)) assert str(event) == expected_str - def test_from_github(self): - repository_mock = Repository() - repository_mock.name = "Test Repo" - repository_mock.created_at = "2024-01-01" - repository_mock.updated_at = "2024-12-24" - repository_mock.title = "Nest" - repository_mock.pitch = "Nest Pitch" - repository_mock.tags = ["react", "python"] - - event = Event() - - with patch( - "apps.owasp.models.event.RepositoryBasedEntityModel.from_github" - ) as mock_from_github: - mock_from_github.side_effect = lambda instance, _, repo: setattr( - instance, "name", repo.title - ) - - event.from_github(repository_mock) - - assert event.owasp_repository == repository_mock - - mock_from_github.assert_called_once_with( - event, - { - "description": "pitch", - "level": "level", - "name": "title", - "tags": "tags", - }, - repository_mock, - ) - - assert event.name == repository_mock.title - def test_bulk_save(self): - mock_event = [Mock(id=None), Mock(id=1)] + mock_event = [ + Mock(id=None, start_date=date(2025, 1, 1)), + Mock(id=1, start_date=date(2025, 1, 1)), + ] with patch("apps.owasp.models.event.BulkSaveModel.bulk_save") as mock_bulk_save: Event.bulk_save(mock_event, fields=["name"]) mock_bulk_save.assert_called_once_with(Event, mock_event, fields=["name"]) - - @patch("apps.owasp.models.event.Event.objects.get") - def test_update_data_event_does_not_exist(self, mock_get): - mock_get.side_effect = Event.DoesNotExist - gh_repository_mock = Mock() - gh_repository_mock.name = "new_repo" - repository_mock = Repository() - - with patch.object(Event, "save", return_value=None) as mock_save: - event = Event.update_data(gh_repository_mock, repository_mock, save=True) - mock_save.assert_called_once() - assert event.key == "new_repo" - assert event.owasp_repository == repository_mock diff --git a/backend/tests/slack/commands/events_test.py b/backend/tests/slack/commands/events_test.py index 8701baac8..55cc5f778 100644 --- a/backend/tests/slack/commands/events_test.py +++ b/backend/tests/slack/commands/events_test.py @@ -12,18 +12,18 @@ "name": "OWASP Snow 2025", "category": "AppSec Days", "startDate": "2025-03-14", - "dates": "March 14, 2025", + "endDate": "March 14, 2025", "url": "https://example.com/snow", - "optionalText": "Regional conference", + "description": "Regional conference", "categoryDescription": "Local events description", }, { "name": "OWASP Global AppSec EU 2025", "category": "Global", "startDate": "2025-05-26", - "dates": "May 26-30, 2025", + "endDate": "May 26-30, 2025", "url": "https://example.com/eu", - "optionalText": "Premier conference", + "description": "Premier conference", "categoryDescription": "Global events description", }, ] @@ -92,7 +92,7 @@ def test_handler_responses( event_block = blocks[current_block]["text"]["text"] assert "*1. *" in event_block assert "Start Date: 2025-03-14" in event_block - assert "Duration: March 14, 2025" in event_block + assert "End Date: March 14, 2025" in event_block assert "_Regional conference_" in event_block current_block += 1 From e951eb0906129dcb994ea75f8aecd0f209a2cf0a Mon Sep 17 00:00:00 2001 From: Abhay Mishra Date: Thu, 20 Feb 2025 23:10:12 +0530 Subject: [PATCH 11/17] removed extra fields from the serializer --- backend/apps/owasp/api/event.py | 2 -- backend/tests/owasp/api/event_test.py | 4 ---- 2 files changed, 6 deletions(-) diff --git a/backend/apps/owasp/api/event.py b/backend/apps/owasp/api/event.py index 2420d1d90..8683c0c19 100644 --- a/backend/apps/owasp/api/event.py +++ b/backend/apps/owasp/api/event.py @@ -15,8 +15,6 @@ class Meta: "name", "description", "url", - "created_at", - "updated_at", ) diff --git a/backend/tests/owasp/api/event_test.py b/backend/tests/owasp/api/event_test.py index 8982502b1..f2a6adb36 100644 --- a/backend/tests/owasp/api/event_test.py +++ b/backend/tests/owasp/api/event_test.py @@ -11,8 +11,6 @@ "name": "Test Event", "description": "A test event", "url": "https://github.com/owasp/Nest", - "created_at": "2024-11-01T00:00:00Z", - "updated_at": "2024-07-02T00:00:00Z", }, True, ), @@ -21,8 +19,6 @@ "name": "biggest event", "description": "this is a biggest event", "url": "https://github.com/owasp", - "created_at": "2023-12-01T00:00:00Z", - "updated_at": "2023-09-02T00:00:00Z", }, True, ), From 9b37d90c1ec48e5a8d439a04580f885dd56fbe1f Mon Sep 17 00:00:00 2001 From: Abhay Date: Fri, 28 Feb 2025 07:37:24 +0530 Subject: [PATCH 12/17] Update backend/apps/owasp/models/event.py Co-authored-by: Arkadii Yakovets <2201626+arkid15r@users.noreply.github.com> --- backend/apps/owasp/models/event.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/apps/owasp/models/event.py b/backend/apps/owasp/models/event.py index 73f6d0ede..34997db7d 100644 --- a/backend/apps/owasp/models/event.py +++ b/backend/apps/owasp/models/event.py @@ -35,7 +35,7 @@ class Meta: key = models.CharField(verbose_name="Key", max_length=100, unique=True) name = models.CharField(verbose_name="Name", max_length=100) description = models.TextField(verbose_name="Description", default="", blank=True) - start_date = models.DateField(verbose_name="Start Date", default="2025-01-01", blank=True) + start_date = models.DateField(verbose_name="Start Date") url = models.URLField(verbose_name="URL", default="", blank=True) def __str__(self): From 216a6daf58bd23dce2aecd4d61dda3f80540e0ac Mon Sep 17 00:00:00 2001 From: Abhay Mishra Date: Fri, 28 Feb 2025 14:14:27 +0530 Subject: [PATCH 13/17] resolved suggestions --- backend/apps/owasp/graphql/nodes/event.py | 1 - backend/apps/owasp/graphql/queries/event.py | 6 ++--- .../commands/owasp_update_events.py | 2 -- ... 0016_remove_event_created_at_and_more.py} | 19 ++++++++------ backend/apps/owasp/models/event.py | 3 --- backend/apps/slack/commands/events.py | 26 +++++++++---------- backend/apps/slack/utils.py | 25 ++++-------------- 7 files changed, 31 insertions(+), 51 deletions(-) rename backend/apps/owasp/migrations/{0015_remove_event_created_at_and_more.py => 0016_remove_event_created_at_and_more.py} (83%) diff --git a/backend/apps/owasp/graphql/nodes/event.py b/backend/apps/owasp/graphql/nodes/event.py index d8c122a5b..e20023521 100644 --- a/backend/apps/owasp/graphql/nodes/event.py +++ b/backend/apps/owasp/graphql/nodes/event.py @@ -11,7 +11,6 @@ class Meta: model = Event fields = ( "category", - "category_description", "end_date", "description", "key", diff --git a/backend/apps/owasp/graphql/queries/event.py b/backend/apps/owasp/graphql/queries/event.py index 6a638c2a6..2b5eda30f 100644 --- a/backend/apps/owasp/graphql/queries/event.py +++ b/backend/apps/owasp/graphql/queries/event.py @@ -1,6 +1,6 @@ """OWASP event GraphQL queries.""" -from datetime import datetime, timezone +from datetime import timezone import graphene @@ -16,8 +16,8 @@ class EventQuery(BaseQuery): def resolve_events(root, info): """Resolve all events.""" - today = datetime.now(timezone.utc).date() + today = timezone.now() - base_query = Event.objects.exclude(start_date__isnull=True).filter(start_date__gte=today) + base_query = Event.objects.filter(start_date__gte=today) return base_query.order_by("start_date") diff --git a/backend/apps/owasp/management/commands/owasp_update_events.py b/backend/apps/owasp/management/commands/owasp_update_events.py index dc5a36934..344cae291 100644 --- a/backend/apps/owasp/management/commands/owasp_update_events.py +++ b/backend/apps/owasp/management/commands/owasp_update_events.py @@ -20,7 +20,6 @@ def handle(self, *args, **kwargs): 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", "")) @@ -31,7 +30,6 @@ def handle(self, *args, **kwargs): fields = { "category": get_event_category(category_name), - "category_description": category_description, "end_date": end_date, "key": key, "name": event_data.get("name", ""), diff --git a/backend/apps/owasp/migrations/0015_remove_event_created_at_and_more.py b/backend/apps/owasp/migrations/0016_remove_event_created_at_and_more.py similarity index 83% rename from backend/apps/owasp/migrations/0015_remove_event_created_at_and_more.py rename to backend/apps/owasp/migrations/0016_remove_event_created_at_and_more.py index 0bf4e7db2..9bf0bcbf1 100644 --- a/backend/apps/owasp/migrations/0015_remove_event_created_at_and_more.py +++ b/backend/apps/owasp/migrations/0016_remove_event_created_at_and_more.py @@ -1,11 +1,13 @@ -# Generated by Django 5.1.5 on 2025-02-20 16:06 +# Generated by Django 5.1.5 on 2025-02-28 07:53 + +import datetime from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ("owasp", "0014_project_custom_tags"), + ("owasp", "0015_snapshot"), ] operations = [ @@ -60,11 +62,6 @@ class Migration(migrations.Migration): 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="end_date", @@ -73,7 +70,13 @@ class Migration(migrations.Migration): migrations.AddField( model_name="event", name="start_date", - field=models.DateField(blank=True, default="2025-01-01", verbose_name="Start Date"), + field=models.DateField( + default=datetime.datetime( + 2025, 2, 28, 7, 53, 17, 155842, tzinfo=datetime.timezone.utc + ), + verbose_name="Start Date", + ), + preserve_default=False, ), migrations.AlterField( model_name="event", diff --git a/backend/apps/owasp/models/event.py b/backend/apps/owasp/models/event.py index 34997db7d..77ac5a728 100644 --- a/backend/apps/owasp/models/event.py +++ b/backend/apps/owasp/models/event.py @@ -28,9 +28,6 @@ class Meta: default=EventCategory.OTHER, ) - category_description = models.TextField( - verbose_name="Category Description", default="", blank=True - ) end_date = models.DateField(verbose_name="End Date", null=True, blank=True) key = models.CharField(verbose_name="Key", max_length=100, unique=True) name = models.CharField(verbose_name="Name", max_length=100) diff --git a/backend/apps/slack/commands/events.py b/backend/apps/slack/commands/events.py index 141f3d115..6819614c6 100644 --- a/backend/apps/slack/commands/events.py +++ b/backend/apps/slack/commands/events.py @@ -19,15 +19,14 @@ def events_handler(ack, command, client): 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"]) + valid_events = [event for event in events_data if event.start_date] + sorted_events = sorted(valid_events, key=lambda x: x.start_date) categorized_events = {} for event in sorted_events: - category = event.get("category") or "Other" + category = event.category or "Other" if category not in categorized_events: categorized_events[category] = { - "description": event.get("categoryDescription", ""), "events": [], } categorized_events[category]["events"].append(event) @@ -37,22 +36,21 @@ def events_handler(ack, command, client): blocks.append({"type": "divider"}) for category, category_data in categorized_events.items(): - blocks.append(markdown(f"*{category} Events:*{NL}{category_data['description']}{NL}")) + blocks.append(markdown(f"*Category: {category.replace('_', ' ').title()}*")) for idx, event in enumerate(category_data["events"], 1): - if event.get("url"): - block_text = f"*{idx}. <{event['url']}|{event['name']}>*{NL}" + if event.url: + block_text = f"*{idx}. <{event.url}|{event.name}>*{NL}" else: - block_text = f"*{idx}. {event['name']}*{NL}" + block_text = f"*{idx}. {event.name}*{NL}" - if event.get("startDate"): - block_text += f" Start Date: {event['startDate']}{NL}" + block_text += f" Start Date: {event.start_date}{NL}" - if event.get("endDate"): - block_text += f" End Date: {event['endDate']}{NL}" + if event.end_date: + block_text += f" End Date: {event.end_date}{NL}" - if event.get("description"): - block_text += f"_{event['description']}_{NL}" + if event.description: + block_text += f"_{event.description}_{NL}" blocks.append(markdown(block_text)) diff --git a/backend/apps/slack/utils.py b/backend/apps/slack/utils.py index d81887016..84ddbed38 100644 --- a/backend/apps/slack/utils.py +++ b/backend/apps/slack/utils.py @@ -6,9 +6,9 @@ from html import escape as escape_html from urllib.parse import urljoin -import graphene import requests import yaml +from django.utils import timezone from lxml import html from requests.exceptions import RequestException @@ -86,27 +86,12 @@ def get_staff_data(timeout=30): def get_events_data(): """Get raw events data via GraphQL.""" - from apps.owasp.graphql.queries.event import EventQuery - - query = """ - query { - events { - key - name - category - endDate - startDate - url - description - categoryDescription - } - } - """ + from apps.owasp.models.event import Event + try: - result = graphene.Schema(query=EventQuery).execute(query) - return result.data["events"] + return Event.objects.filter(start_date__gte=timezone.now()).order_by("start_date") except Exception as e: - logger.exception("Failed to fetch events data via GraphQL", extra={"error": str(e)}) + logger.exception("Failed to fetch events data via database", extra={"error": str(e)}) return None From 337173ce40ff3173966051a6dd2421c282dd6053 Mon Sep 17 00:00:00 2001 From: Abhay Mishra Date: Fri, 28 Feb 2025 14:44:48 +0530 Subject: [PATCH 14/17] fix testcase --- backend/tests/slack/commands/events_test.py | 61 +++++++++++++-------- 1 file changed, 39 insertions(+), 22 deletions(-) diff --git a/backend/tests/slack/commands/events_test.py b/backend/tests/slack/commands/events_test.py index 55cc5f778..efe6a4d8b 100644 --- a/backend/tests/slack/commands/events_test.py +++ b/backend/tests/slack/commands/events_test.py @@ -7,25 +7,36 @@ from apps.slack.commands.events import events_handler + +# Define a mock event class to simulate the new event object structure +class MockEvent: + def __init__(self, name, category, start_date, end_date, url, description): + self.name = name + self.category = category + self.start_date = start_date + self.end_date = end_date + self.url = url + self.description = description + + +# Mock event data as objects mock_events = [ - { - "name": "OWASP Snow 2025", - "category": "AppSec Days", - "startDate": "2025-03-14", - "endDate": "March 14, 2025", - "url": "https://example.com/snow", - "description": "Regional conference", - "categoryDescription": "Local events description", - }, - { - "name": "OWASP Global AppSec EU 2025", - "category": "Global", - "startDate": "2025-05-26", - "endDate": "May 26-30, 2025", - "url": "https://example.com/eu", - "description": "Premier conference", - "categoryDescription": "Global events description", - }, + MockEvent( + name="OWASP Snow 2025", + category="AppSec Days", + start_date="2025-03-14", + end_date="March 14, 2025", + url="https://example.com/snow", + description="Regional conference", + ), + MockEvent( + name="OWASP Global AppSec EU 2025", + category="Global", + start_date="2025-05-26", + end_date="May 26-30, 2025", + url="https://example.com/eu", + description="Premier conference", + ), ] @@ -85,8 +96,7 @@ def test_handler_responses( if has_events_data: current_block = 2 - assert "*AppSec Days Events:*" in blocks[current_block]["text"]["text"] - assert "Local events description" in blocks[current_block]["text"]["text"] + assert "*Category: Appsec Days*" in blocks[current_block]["text"]["text"] current_block += 1 event_block = blocks[current_block]["text"]["text"] @@ -99,8 +109,15 @@ def test_handler_responses( assert blocks[current_block]["type"] == "divider" current_block += 1 - assert "*Global Events:*" in blocks[current_block]["text"]["text"] - assert "Global events description" in blocks[current_block]["text"]["text"] + assert "*Category: Global*" in blocks[current_block]["text"]["text"] + current_block += 1 + + event_block = blocks[current_block]["text"]["text"] + assert "*1. *" in event_block + assert "Start Date: 2025-05-26" in event_block + assert "End Date: May 26-30, 2025" in event_block + assert "_Premier conference_" in event_block + current_block += 1 footer_block = blocks[-1]["text"]["text"] assert "🔍 For more information about upcoming events" in footer_block From 68d4d84d4a47c5a054ad0cc7201665ef62eef61a Mon Sep 17 00:00:00 2001 From: Abhay Mishra Date: Sun, 2 Mar 2025 02:24:39 +0530 Subject: [PATCH 15/17] resolved suggestion --- backend/Makefile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/Makefile b/backend/Makefile index c1b05d2ee..5cc342fc1 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -88,11 +88,11 @@ owasp-scrape-committees: owasp-scrape-projects: @echo "Scraping OWASP site projects data" - @CMD="poetry run python manage.py owasp_scrape_projects" $(MAKE) exec-backend-command + @CMD="python manage.py owasp_scrape_projects" $(MAKE) exec-backend-command owasp-update-events: @echo "Getting OWASP events data" - @CMD="poetry run python manage.py owasp_update_events" $(MAKE) exec-backend-command + @CMD="python manage.py owasp_update_events" $(MAKE) exec-backend-command purge-data: @CMD="python manage.py purge_data" $(MAKE) exec-backend-command @@ -101,7 +101,7 @@ recreate-schema: @CMD="psql -U nest_user_dev -d nest_db_dev -c \ 'DROP SCHEMA public CASCADE; CREATE SCHEMA public; GRANT ALL ON SCHEMA public TO nest_user_dev'" \ $(MAKE) exec-db-command 2>/dev/null - $(MAKE) migrateNestBot goes directly to the DB + $(MAKE) migrate setup: @CMD="python manage.py createsuperuser" $(MAKE) exec-backend-command-it From e6d7be40d74f0ad47ebd714ff719df18bb3eaf22 Mon Sep 17 00:00:00 2001 From: Arkadii Yakovets Date: Sun, 2 Mar 2025 12:05:21 -0800 Subject: [PATCH 16/17] Update code --- backend/apps/common/utils.py | 8 ++ backend/apps/github/utils.py | 2 +- backend/apps/owasp/graphql/queries/event.py | 12 +- .../commands/owasp_update_events.py | 71 +++--------- .../migrations/0018_merge_20250302_1945.py | 12 ++ backend/apps/owasp/models/event.py | 107 ++++++++++++++++-- backend/apps/owasp/utils.py | 53 --------- backend/poetry.lock | 2 +- backend/pyproject.toml | 1 + 9 files changed, 137 insertions(+), 131 deletions(-) create mode 100644 backend/apps/owasp/migrations/0018_merge_20250302_1945.py delete mode 100644 backend/apps/owasp/utils.py diff --git a/backend/apps/common/utils.py b/backend/apps/common/utils.py index c80ed2bda..4523ce6e2 100644 --- a/backend/apps/common/utils.py +++ b/backend/apps/common/utils.py @@ -1,10 +1,12 @@ """Common app utils.""" +import re from datetime import datetime, timezone from django.conf import settings from django.template.defaultfilters import pluralize from django.utils.text import Truncator +from django.utils.text import slugify as django_slugify from humanize import intword, naturaltime @@ -35,6 +37,7 @@ def natural_date(value): value = datetime.strptime(value, "%Y-%m-%d").replace(tzinfo=timezone.utc) elif isinstance(value, int): value = datetime.fromtimestamp(value, tz=timezone.utc) + return naturaltime(value) @@ -44,6 +47,11 @@ def natural_number(value, unit=None): return f"{number} {unit}{pluralize(value)}" if unit else number +def slugify(text): + """Return slug for text.""" + return re.sub(r"-{2,}", "-", django_slugify(text)) + + def truncate(text, limit, truncate="..."): """Truncate text to the given limit.""" return Truncator(text).chars(limit, truncate=truncate) diff --git a/backend/apps/github/utils.py b/backend/apps/github/utils.py index a98c38fc9..23502ac8d 100644 --- a/backend/apps/github/utils.py +++ b/backend/apps/github/utils.py @@ -46,7 +46,7 @@ def get_repository_path(url): def normalize_url(url, check_path=False): - """Normalize GitHub URL.""" + """Normalize URL.""" parsed_url = urlparse(url) if not parsed_url.netloc or (check_path and not parsed_url.path): return None diff --git a/backend/apps/owasp/graphql/queries/event.py b/backend/apps/owasp/graphql/queries/event.py index 2b5eda30f..f9f3edc48 100644 --- a/backend/apps/owasp/graphql/queries/event.py +++ b/backend/apps/owasp/graphql/queries/event.py @@ -1,7 +1,5 @@ """OWASP event GraphQL queries.""" -from datetime import timezone - import graphene from apps.common.graphql.queries import BaseQuery @@ -12,12 +10,8 @@ class EventQuery(BaseQuery): """Event queries.""" - events = graphene.List(EventNode) + upcoming_events = graphene.List(EventNode) - def resolve_events(root, info): + def resolve_upcoming_events(root, info): """Resolve all events.""" - today = timezone.now() - - base_query = Event.objects.filter(start_date__gte=today) - - return base_query.order_by("start_date") + return Event.upcoming_events diff --git a/backend/apps/owasp/management/commands/owasp_update_events.py b/backend/apps/owasp/management/commands/owasp_update_events.py index 344cae291..8ea14dc08 100644 --- a/backend/apps/owasp/management/commands/owasp_update_events.py +++ b/backend/apps/owasp/management/commands/owasp_update_events.py @@ -2,66 +2,25 @@ 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, EventCategory -from apps.owasp.utils import parse_event_dates +from apps.github.utils import get_repository_file_content +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) - events = [] - - for category in data: - category_name = category.get("category", "") - - for event_data in category["events"]: - event_name_slug = slugify(event_data.get("name", "")) - key = event_name_slug - end_date = parse_event_dates( - event_data.get("dates", ""), event_data.get("start-date") - ) - - fields = { - "category": get_event_category(category_name), - "end_date": end_date, - "key": key, - "name": event_data.get("name", ""), - "description": event_data.get("optional-text", ""), - "start_date": event_data.get("start-date", None), - "url": normalize_url(event_data.get("url", "")) or "", - } - - try: - event = Event.objects.get(key=key) - # Update existing event - for key, value in fields.items(): - setattr(event, key, value) - events.append(event) - except Event.DoesNotExist: - # Create new event - event = Event(**fields) - events.append(event) - - if events: - self.stdout.write(f"Saving {len(events)} events...") - Event.bulk_save(events, fields) - self.stdout.write(self.style.SUCCESS("Successfully saved events")) - else: - self.stdout.write(self.style.WARNING("No events to save")) - - -def get_event_category(category_name): - """Get event category.""" - category_map = { - "Global": EventCategory.GLOBAL, - "AppSec Days": EventCategory.APPSEC_DAYS, - "Partner": EventCategory.PARTNER, - } - return category_map.get(category_name, EventCategory.OTHER) + data = yaml.safe_load( + get_repository_file_content( + "https://raw.githubusercontent.com/OWASP/owasp.github.io/main/_data/events.yml" + ) + ) + + Event.bulk_save( + [ + Event.update_data(category["category"], event_data) + for category in data + for event_data in category["events"] + ] + ) diff --git a/backend/apps/owasp/migrations/0018_merge_20250302_1945.py b/backend/apps/owasp/migrations/0018_merge_20250302_1945.py new file mode 100644 index 000000000..69b3496e8 --- /dev/null +++ b/backend/apps/owasp/migrations/0018_merge_20250302_1945.py @@ -0,0 +1,12 @@ +# Generated by Django 5.1.6 on 2025-03-02 19:45 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("owasp", "0016_remove_event_created_at_and_more"), + ("owasp", "0017_projecthealthrequirements_and_more"), + ] + + operations = [] diff --git a/backend/apps/owasp/models/event.py b/backend/apps/owasp/models/event.py index 77ac5a728..86113d779 100644 --- a/backend/apps/owasp/models/event.py +++ b/backend/apps/owasp/models/event.py @@ -1,17 +1,12 @@ """OWASP app event model.""" +from dateutil import parser from django.db import models +from django.utils import timezone from apps.common.models import BulkSaveModel, TimestampedModel - - -class EventCategory(models.TextChoices): - """Event category choices.""" - - GLOBAL = "global", "Global" - APPSEC_DAYS = "appsec_days", "AppSec Days" - PARTNER = "partner", "Partner" - OTHER = "other", "Other" +from apps.common.utils import slugify +from apps.github.utils import normalize_url class Event(BulkSaveModel, TimestampedModel): @@ -21,11 +16,19 @@ class Meta: db_table = "owasp_events" verbose_name_plural = "Events" + class Category(models.TextChoices): + """Event category.""" + + APPSEC_DAYS = "appsec_days", "AppSec Days" + GLOBAL = "global", "Global" + OTHER = "other", "Other" + PARTNER = "partner", "Partner" + category = models.CharField( verbose_name="Category", max_length=20, - choices=EventCategory.choices, - default=EventCategory.OTHER, + choices=Category.choices, + default=Category.OTHER, ) end_date = models.DateField(verbose_name="End Date", null=True, blank=True) @@ -39,7 +42,89 @@ def __str__(self): """Event human readable representation.""" return f"{self.name or self.key}" + @staticmethod + def upcoming_events(): + """Get upcoming events.""" + return Event.objects.filter(start_date__gte=timezone.now().date()).order_by("start_date") + @staticmethod def bulk_save(events, fields=None): """Bulk save events.""" BulkSaveModel.bulk_save(Event, events, fields=fields) + + # TODO(arkid15r): refactor this when there is a chance. + @staticmethod + def parse_dates(dates, start_date): + """Parse event dates.""" + if not dates: + return None + + # Handle single-day events (e.g., "March 14, 2025") + if "," in dates and "-" not in dates: + try: + return parser.parse(dates).date() + except ValueError: + return None + + # Handle date ranges (e.g., "May 26-30, 2025" or "November 2-6, 2026") + if "-" in dates and "," in dates: + try: + # Split the date range into parts + date_range, year = dates.split(", ") + month_day_range = date_range.split() + + # Extract month and day range + month = month_day_range[0] + day_range = month_day_range[1] + + # Extract end day from the range + end_day = int(day_range.split("-")[-1]) + + # Parse the year + year = int(year.strip()) + + # Use the start_date to determine the month if provided + if start_date: + start_date_parsed = start_date + month = start_date_parsed.strftime("%B") # Full month name (e.g., "May") + + # Parse the full end date string + end_date_str = f"{month} {end_day}, {year}" + return parser.parse(end_date_str).date() + except (ValueError, IndexError, AttributeError): + return None + + return None + + @staticmethod + def update_data(category, data, save=True): + """Update event data.""" + key = slugify(data["name"]) + try: + event = Event.objects.get(key=key) + except Event.DoesNotExist: + event = Event(key=key) + + event.from_dict(category, data) + if save: + event.save() + + return event + + def from_dict(self, category, data): + """Update instance based on the dict data.""" + fields = { + "category": { + "AppSec Days": Event.Category.APPSEC_DAYS, + "Global": Event.Category.GLOBAL, + "Partner": Event.Category.PARTNER, + }.get(category, Event.Category.OTHER), + "description": data.get("optional-text", ""), + "end_date": Event.parse_dates(data.get("dates", ""), data.get("start-date")), + "name": data["name"], + "start_date": data["start-date"], + "url": normalize_url(data.get("url", "")) or "", + } + + for key, value in fields.items(): + setattr(self, key, value) diff --git a/backend/apps/owasp/utils.py b/backend/apps/owasp/utils.py deleted file mode 100644 index 3b2e12f5f..000000000 --- a/backend/apps/owasp/utils.py +++ /dev/null @@ -1,53 +0,0 @@ -"""OWASP utils.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from datetime import date - -from dateutil import parser - - -def parse_event_dates(dates: str, start_date: str | None = None) -> date | None: - """Parse event dates.""" - if not dates: - return None - - # Handle single-day events (e.g., "March 14, 2025") - if "," in dates and "-" not in dates: - try: - return parser.parse(dates).date() - except ValueError: - return None - - # Handle date ranges (e.g., "May 26-30, 2025" or "November 2-6, 2026") - if "-" in dates and "," in dates: - try: - # Split the date range into parts - date_range, year = dates.split(", ") - month_day_range = date_range.split() - - # Extract month and day range - month = month_day_range[0] - day_range = month_day_range[1] - - # Extract end day from the range - end_day = int(day_range.split("-")[-1]) - - # Parse the year - year = int(year.strip()) - - # Use the start_date to determine the month if provided - if start_date: - start_date_parsed = start_date - month = start_date_parsed.strftime("%B") # Full month name (e.g., "May") - - # Parse the full end date string - end_date_str = f"{month} {end_day}, {year}" - return parser.parse(end_date_str).date() - except (ValueError, IndexError, AttributeError): - return None - - return None diff --git a/backend/poetry.lock b/backend/poetry.lock index 3d7e084ad..c58641009 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -2915,4 +2915,4 @@ propcache = ">=0.2.0" [metadata] lock-version = "2.1" python-versions = "^3.13" -content-hash = "826daea6bad6281b7716210d47a83bcf408a6e0ab48e97acc71207712e596fb8" +content-hash = "321bab241f908fcedf7aa3c9bedc87c62e5f49df60b955ed2f53cdee31dc8c64" diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 493882762..5bc4eb439 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -39,6 +39,7 @@ openai = "^1.60.1" psycopg = "^3.2.4" pygithub = "^2.5.0" python = "^3.13" +python-dateutil = "^2.9.0.post0" pyyaml = "^6.0.2" requests = "^2.32.3" sentry-sdk = { extras = ["django"], version = "^2.20.0" } From e6c3e1c319aa1ea29b621be09c17d423fc570c8c Mon Sep 17 00:00:00 2001 From: Arkadii Yakovets Date: Sun, 2 Mar 2025 12:21:41 -0800 Subject: [PATCH 17/17] Apply suggestion --- backend/apps/owasp/models/event.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/apps/owasp/models/event.py b/backend/apps/owasp/models/event.py index 86113d779..81e9e4377 100644 --- a/backend/apps/owasp/models/event.py +++ b/backend/apps/owasp/models/event.py @@ -122,7 +122,9 @@ def from_dict(self, category, data): "description": data.get("optional-text", ""), "end_date": Event.parse_dates(data.get("dates", ""), data.get("start-date")), "name": data["name"], - "start_date": data["start-date"], + "start_date": parser.parse(data["start-date"]).date() + if isinstance(data["start-date"], str) + else data["start-date"], "url": normalize_url(data.get("url", "")) or "", }