From 2666e5886c56f3c60c03275f41594116f87113c2 Mon Sep 17 00:00:00 2001 From: David Slusser Date: Thu, 20 Aug 2026 21:00:00 -0700 Subject: [PATCH 1/3] increasing look ahead for eventbrite events --- src/django_project/core/settings.py | 2 + .../tests/unit/web/test_eventbrite_scraper.py | 66 +++++++++++++++++++ .../tests/unit/web/test_tasks.py | 16 ++++- .../web/scripts/ingest_events.py | 4 +- src/django_project/web/signals.py | 38 +++++------ src/django_project/web/tasks.py | 5 +- .../web/utilities/scrapers/eventbrite.py | 9 +-- 7 files changed, 113 insertions(+), 27 deletions(-) create mode 100644 src/django_project/tests/unit/web/test_eventbrite_scraper.py diff --git a/src/django_project/core/settings.py b/src/django_project/core/settings.py index 1a1ff2f..8cb6e19 100644 --- a/src/django_project/core/settings.py +++ b/src/django_project/core/settings.py @@ -288,6 +288,7 @@ # third party integrations DISCORD_WEBHOOK_URL: str | None = env.str("DISCORD_WEBHOOK_URL", None) # for the general channel EVENTBRITE_API_KEY: str | None = env.str("EVENTBRITE_API_KEY", None) +EVENTBRITE_EVENT_LOOKAHEAD_DAYS: int = env.int("EVENTBRITE_EVENT_LOOKAHEAD_DAYS", 60) GEMINI_API_KEY: str | None = env.str("GEMINI_API_KEY", None) GOOGLE_ANALYTICS_TAG_ID: str | None = env.str("GOOGLE_ANALYTICS_TAG_ID", None) LINKEDIN_ACCESS_TOKEN: str | None = env.str("LINKEDIN_ACCESS_TOKEN", None) @@ -295,5 +296,6 @@ LINKEDIN_CLIENT_SECRET: str | None = env.str("LINKEDIN_CLIENT_SECRET", None) LINKEDIN_ORGANIZATION_URN: str | None = env.str("LINKEDIN_ORGANIZATION_URN", None) LINKEDIN_REFRESH_TOKEN: str | None = env.str("LINKEDIN_REFRESH_TOKEN", None) +POST_TO_LINKEDIN: bool = env.bool("POST_TO_LINKEDIN", False) SPUG_API_TOKEN: str | None = env.str("SPUG_API_TOKEN", None) SPUG_API_URL: str | None = env.str("SPUG_API_URL", None) diff --git a/src/django_project/tests/unit/web/test_eventbrite_scraper.py b/src/django_project/tests/unit/web/test_eventbrite_scraper.py new file mode 100644 index 0000000..e9ec2b9 --- /dev/null +++ b/src/django_project/tests/unit/web/test_eventbrite_scraper.py @@ -0,0 +1,66 @@ +import os +from datetime import UTC, datetime +from pathlib import Path +from unittest.mock import Mock, patch + +import django + +BASE_DIR = Path(__file__).parents[4] +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings") +os.environ.setdefault("ENV_PATH", f"{BASE_DIR}/envs/.env.test") + +django.setup() + +from web.utilities.scrapers.eventbrite import get_events_for_organization + + +class TestGetEventsForOrganization: + @patch("web.utilities.scrapers.eventbrite.requests.get") + @patch("web.utilities.scrapers.eventbrite.timezone.now") + def test_uses_configured_lookahead_window(self, mock_now, mock_get): + mock_now.return_value = datetime(2026, 8, 21, 7, 11, 15, tzinfo=UTC) + response = Mock() + response.json.return_value = {"events": [{"id": "evt_123"}]} + response.raise_for_status.return_value = None + mock_get.return_value = response + + with patch("web.utilities.scrapers.eventbrite.settings.EVENTBRITE_API_KEY", "token", create=True): + with patch( + "web.utilities.scrapers.eventbrite.settings.EVENTBRITE_EVENT_LOOKAHEAD_DAYS", + 60, + create=True, + ): + events = get_events_for_organization("637389713") + + assert events == [{"id": "evt_123"}] + mock_get.assert_called_once_with( + "https://www.eventbriteapi.com/v3/organizers/637389713/events/" + "?start_date.range_start=2026-08-21T07:11:15Z&start_date.range_end=2026-10-20T07:11:15Z", + headers={"Authorization": "Bearer token"}, + timeout=15, + ) + + @patch("web.utilities.scrapers.eventbrite.requests.get") + @patch("web.utilities.scrapers.eventbrite.timezone.now") + def test_explicit_age_overrides_configured_lookahead(self, mock_now, mock_get): + mock_now.return_value = datetime(2026, 8, 21, 7, 11, 15, tzinfo=UTC) + response = Mock() + response.json.return_value = {"events": [{"id": "evt_456"}]} + response.raise_for_status.return_value = None + mock_get.return_value = response + + with patch("web.utilities.scrapers.eventbrite.settings.EVENTBRITE_API_KEY", "token", create=True): + with patch( + "web.utilities.scrapers.eventbrite.settings.EVENTBRITE_EVENT_LOOKAHEAD_DAYS", + 60, + create=True, + ): + events = get_events_for_organization("637389713", age=14) + + assert events == [{"id": "evt_456"}] + mock_get.assert_called_once_with( + "https://www.eventbriteapi.com/v3/organizers/637389713/events/" + "?start_date.range_start=2026-08-21T07:11:15Z&start_date.range_end=2026-09-04T07:11:15Z", + headers={"Authorization": "Bearer token"}, + timeout=15, + ) diff --git a/src/django_project/tests/unit/web/test_tasks.py b/src/django_project/tests/unit/web/test_tasks.py index 39d6c5b..d31eaf0 100644 --- a/src/django_project/tests/unit/web/test_tasks.py +++ b/src/django_project/tests/unit/web/test_tasks.py @@ -12,7 +12,7 @@ django.setup() from model_bakery import baker from web.models import Event -from web.tasks import ingest_future_eventbrite_events +from web.tasks import ingest_future_eventbrite_events, post_event_to_linkedin class TestIngestFutureEventbriteEvents(TestCase): @@ -75,3 +75,17 @@ def test_ingests_event_with_null_primary_venue_address(self, mock_get_events_for self.assertEqual(event.location_name, "Online") self.assertEqual(event.location_address, "") self.assertEqual(event.map_link, "") + + +class TestPostEventToLinkedIn(TestCase): + def test_skips_when_post_to_linkedin_setting_is_false(self): + event = baker.make("web.Event") + + with ( + patch("web.tasks.settings.POST_TO_LINKEDIN", False), + patch("web.tasks.LinkedInOrganizationClient") as mock_linkedin_client, + ): + result = post_event_to_linkedin(event.pk, is_new=True) + + self.assertEqual(result, f"POST_TO_LINKEDIN is False. Skipping LinkedIn post for event with pk {event.pk}.") + mock_linkedin_client.assert_not_called() diff --git a/src/django_project/web/scripts/ingest_events.py b/src/django_project/web/scripts/ingest_events.py index 789b614..6950eab 100644 --- a/src/django_project/web/scripts/ingest_events.py +++ b/src/django_project/web/scripts/ingest_events.py @@ -6,7 +6,7 @@ def get_eventbright_events(): - tech_group_list = TechGroup.objects.filter(enabled=True, platform__name="Eventbrite") + tech_group_list = TechGroup.objects.filter(enabled=True, platform__name="Eventbrite", name__icontains="launchpad") for group in tech_group_list: print("INFO: getting upcoming events for ", group.name) job = ingest_future_eventbrite_events.s(group.pk) @@ -24,4 +24,4 @@ def get_meetup_events() -> None: def run(): get_eventbright_events() - get_meetup_events() + # get_meetup_events() diff --git a/src/django_project/web/signals.py b/src/django_project/web/signals.py index 43b7a94..0e1d8b8 100644 --- a/src/django_project/web/signals.py +++ b/src/django_project/web/signals.py @@ -1,15 +1,14 @@ -from typing import Any - from django.db import transaction from django.db.models.signals import post_save from django.dispatch import receiver from .models import Event -from .tasks import ( - post_event_to_discord, - post_event_to_linkedin, - post_event_to_spug_task, -) + +# from .tasks import ( +# post_event_to_discord, +# post_event_to_linkedin, +# post_event_to_spug_task, +# ) @receiver(post_save, sender=Event) @@ -20,17 +19,18 @@ def event_post_to_linkedin_signal(sender, instance, created, **kwargs) -> None: if created: def on_commit_callback() -> None: - # post to Discord - discord_job: Any = post_event_to_discord.s(instance.pk, is_new=True) - discord_job.apply_async() - - # post to LinkedIn - linkedin_job: Any = post_event_to_linkedin.s(instance.pk, is_new=True) - linkedin_job.apply_async() - - # if TechGroup is SPUG, also post to SPUG website via api - if instance.group.name == "Spokane Python User Group": - spug_job: Any = post_event_to_spug_task.s(instance.pk) - spug_job.apply_async() + # # post to Discord + # discord_job: Any = post_event_to_discord.s(instance.pk, is_new=True) + # discord_job.apply_async() + + # # post to LinkedIn + # linkedin_job: Any = post_event_to_linkedin.s(instance.pk, is_new=True) + # linkedin_job.apply_async() + + # # if TechGroup is SPUG, also post to SPUG website via api + # if instance.group.name == "Spokane Python User Group": + # spug_job: Any = post_event_to_spug_task.s(instance.pk) + # spug_job.apply_async() + pass transaction.on_commit(on_commit_callback) diff --git a/src/django_project/web/tasks.py b/src/django_project/web/tasks.py index 2806dfc..d5a5bc1 100644 --- a/src/django_project/web/tasks.py +++ b/src/django_project/web/tasks.py @@ -204,12 +204,15 @@ def post_event_to_linkedin(event_pk: int, is_new: bool) -> str: if not event: return f"Event with pk {event_pk} not found." + if not settings.POST_TO_LINKEDIN: + return f"POST_TO_LINKEDIN is False. Skipping LinkedIn post for event with pk {event_pk}." + linkedin_credential = IntegrationCredential.objects.filter(provider="linkedin").first() access_token = linkedin_credential.access_token if linkedin_credential else settings.LINKEDIN_ACCESS_TOKEN refresh_token = linkedin_credential.refresh_token if linkedin_credential else settings.LINKEDIN_REFRESH_TOKEN if not settings.LINKEDIN_ORGANIZATION_URN: - return "LinkedIn organization URN not configured in settings. Skipping post." + return f"LinkedIn organization URN not configured in settings. Skipping post for event with pk {event_pk}." if not access_token and not (refresh_token and settings.LINKEDIN_CLIENT_ID and settings.LINKEDIN_CLIENT_SECRET): return "LinkedIn API credentials not configured in settings. Skipping post." diff --git a/src/django_project/web/utilities/scrapers/eventbrite.py b/src/django_project/web/utilities/scrapers/eventbrite.py index deca1ae..1608e4e 100644 --- a/src/django_project/web/utilities/scrapers/eventbrite.py +++ b/src/django_project/web/utilities/scrapers/eventbrite.py @@ -63,20 +63,21 @@ def get_organization_details(organization_id: str) -> dict: return response.json()["organizers"][0] -def get_events_for_organization(organization_id: str, age: int = 14) -> list: +def get_events_for_organization(organization_id: str, age: int | None = None) -> list: """get a list of events for a given Eventbrite organization Args: organization_id (str): Eventbrite organization identifier - age (int): number of days ago when events were created + age (int | None): number of days ahead to search for upcoming events Returns: - list: list of Eventbrite events created in the past days + list: list of Eventbrite events starting in the next days """ api_token: str | None = getattr(settings, "EVENTBRITE_API_KEY", None) if not api_token: return [] - start_date_range_end: str = (timezone.now() + timedelta(days=age)).strftime("%Y-%m-%dT%H:%M:%SZ") + lookahead_days = age if age is not None else getattr(settings, "EVENTBRITE_EVENT_LOOKAHEAD_DAYS", 60) + start_date_range_end: str = (timezone.now() + timedelta(days=lookahead_days)).strftime("%Y-%m-%dT%H:%M:%SZ") start_date_range_start: str = timezone.now().strftime("%Y-%m-%dT%H:%M:%SZ") url: str = f"https://www.eventbriteapi.com/v3/organizers/{organization_id}/events/?start_date.range_start={start_date_range_start}&start_date.range_end={start_date_range_end}" headers: dict[str, str] = {"Authorization": f"Bearer {api_token}"} From 10bdc67da81a63f8466fd3a379e92abf47050c52 Mon Sep 17 00:00:00 2001 From: David Slusser Date: Thu, 20 Aug 2026 21:11:12 -0700 Subject: [PATCH 2/3] restore commented code --- .../web/scripts/ingest_events.py | 4 +- src/django_project/web/signals.py | 38 +++++++++---------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/django_project/web/scripts/ingest_events.py b/src/django_project/web/scripts/ingest_events.py index 6950eab..789b614 100644 --- a/src/django_project/web/scripts/ingest_events.py +++ b/src/django_project/web/scripts/ingest_events.py @@ -6,7 +6,7 @@ def get_eventbright_events(): - tech_group_list = TechGroup.objects.filter(enabled=True, platform__name="Eventbrite", name__icontains="launchpad") + tech_group_list = TechGroup.objects.filter(enabled=True, platform__name="Eventbrite") for group in tech_group_list: print("INFO: getting upcoming events for ", group.name) job = ingest_future_eventbrite_events.s(group.pk) @@ -24,4 +24,4 @@ def get_meetup_events() -> None: def run(): get_eventbright_events() - # get_meetup_events() + get_meetup_events() diff --git a/src/django_project/web/signals.py b/src/django_project/web/signals.py index 0e1d8b8..43b7a94 100644 --- a/src/django_project/web/signals.py +++ b/src/django_project/web/signals.py @@ -1,14 +1,15 @@ +from typing import Any + from django.db import transaction from django.db.models.signals import post_save from django.dispatch import receiver from .models import Event - -# from .tasks import ( -# post_event_to_discord, -# post_event_to_linkedin, -# post_event_to_spug_task, -# ) +from .tasks import ( + post_event_to_discord, + post_event_to_linkedin, + post_event_to_spug_task, +) @receiver(post_save, sender=Event) @@ -19,18 +20,17 @@ def event_post_to_linkedin_signal(sender, instance, created, **kwargs) -> None: if created: def on_commit_callback() -> None: - # # post to Discord - # discord_job: Any = post_event_to_discord.s(instance.pk, is_new=True) - # discord_job.apply_async() - - # # post to LinkedIn - # linkedin_job: Any = post_event_to_linkedin.s(instance.pk, is_new=True) - # linkedin_job.apply_async() - - # # if TechGroup is SPUG, also post to SPUG website via api - # if instance.group.name == "Spokane Python User Group": - # spug_job: Any = post_event_to_spug_task.s(instance.pk) - # spug_job.apply_async() - pass + # post to Discord + discord_job: Any = post_event_to_discord.s(instance.pk, is_new=True) + discord_job.apply_async() + + # post to LinkedIn + linkedin_job: Any = post_event_to_linkedin.s(instance.pk, is_new=True) + linkedin_job.apply_async() + + # if TechGroup is SPUG, also post to SPUG website via api + if instance.group.name == "Spokane Python User Group": + spug_job: Any = post_event_to_spug_task.s(instance.pk) + spug_job.apply_async() transaction.on_commit(on_commit_callback) From f39c6ed3048d32ffa9e9e94d2197153008ff6b3c Mon Sep 17 00:00:00 2001 From: David Slusser Date: Thu, 20 Aug 2026 21:12:54 -0700 Subject: [PATCH 3/3] updated ruff changes --- src/django_project/web/utilities/scrapers/eventbrite.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/django_project/web/utilities/scrapers/eventbrite.py b/src/django_project/web/utilities/scrapers/eventbrite.py index 1608e4e..a2f5af2 100644 --- a/src/django_project/web/utilities/scrapers/eventbrite.py +++ b/src/django_project/web/utilities/scrapers/eventbrite.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import random import time from datetime import datetime, timedelta