From 0e909cd35811257895f21e470bec3c85012cef7d Mon Sep 17 00:00:00 2001 From: Samyak Jain Date: Sun, 9 Mar 2025 22:24:01 +0530 Subject: [PATCH 1/6] Added AI processing to events --- backend/Makefile | 4 ++ backend/apps/core/models/prompt.py | 10 +++ backend/apps/owasp/graphql/nodes/event.py | 4 ++ .../commands/owasp_enrich_events.py | 52 ++++++++++++++ ...event_latitude_event_longitude_and_more.py | 33 +++++++++ ..._country_event_postal_code_event_region.py | 28 ++++++++ backend/apps/owasp/models/event.py | 67 ++++++++++++++++++- frontend/__tests__/e2e/pages/Home.spec.ts | 4 +- frontend/__tests__/unit/pages/Modal.test.tsx | 1 + frontend/src/api/queries/homeQueries.ts | 6 ++ frontend/src/components/Modal.tsx | 7 +- frontend/src/pages/Contribute.tsx | 1 + frontend/src/pages/Home.tsx | 47 +++++++------ frontend/src/types/event.ts | 1 + frontend/src/types/modal.ts | 1 + 15 files changed, 240 insertions(+), 26 deletions(-) create mode 100644 backend/apps/owasp/management/commands/owasp_enrich_events.py create mode 100644 backend/apps/owasp/migrations/0023_event_latitude_event_longitude_and_more.py create mode 100644 backend/apps/owasp/migrations/0024_event_country_event_postal_code_event_region.py diff --git a/backend/Makefile b/backend/Makefile index 7731affb5a..6d023cb50e 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -70,6 +70,10 @@ owasp-enrich-committees: @echo "Enriching OWASP committees" @CMD="python manage.py owasp_enrich_committees" $(MAKE) exec-backend-command +owasp-enrich-events: + @echo "Enriching OWASP events" + @CMD="python manage.py owasp_enrich_events" $(MAKE) exec-backend-command + owasp-enrich-projects: @echo "Enriching OWASP projects" @CMD="python manage.py owasp_enrich_projects" $(MAKE) exec-backend-command diff --git a/backend/apps/core/models/prompt.py b/backend/apps/core/models/prompt.py index 6616e9e31f..66b6fe7b7f 100644 --- a/backend/apps/core/models/prompt.py +++ b/backend/apps/core/models/prompt.py @@ -71,6 +71,16 @@ def get_owasp_committee_summary(): """Return OWASP committee summary prompt.""" return Prompt.get_text("owasp-committee-summary") + @staticmethod + def get_owasp_event_suggested_location(): + """Return OWASP event suggested location prompt.""" + return Prompt.get_text("owasp-event-suggested-location") + + @staticmethod + def get_owasp_event_summary(): + """Return OWASP event summary prompt.""" + return Prompt.get_text("owasp-event-summary") + @staticmethod def get_owasp_project_summary(): """Return OWASP project summary prompt.""" diff --git a/backend/apps/owasp/graphql/nodes/event.py b/backend/apps/owasp/graphql/nodes/event.py index e20023521e..b74f0b58af 100644 --- a/backend/apps/owasp/graphql/nodes/event.py +++ b/backend/apps/owasp/graphql/nodes/event.py @@ -11,10 +11,14 @@ class Meta: model = Event fields = ( "category", + "country", "end_date", "description", "key", "name", + "postal_code", + "region", "start_date", + "summary", "url", ) diff --git a/backend/apps/owasp/management/commands/owasp_enrich_events.py b/backend/apps/owasp/management/commands/owasp_enrich_events.py new file mode 100644 index 0000000000..6b93b4ae61 --- /dev/null +++ b/backend/apps/owasp/management/commands/owasp_enrich_events.py @@ -0,0 +1,52 @@ +"""A command to enrich events with extra data.""" + +import logging +import time + +from django.core.management.base import BaseCommand + +from apps.core.models.prompt import Prompt +from apps.owasp.models.event import Event + +logger = logging.getLogger(__name__) + + +class Command(BaseCommand): + help = "Enrich events with extra data." + + def add_arguments(self, parser): + parser.add_argument("--offset", default=0, required=False, type=int) + + def handle(self, *args, **options): + events = Event.objects.order_by("id") + events_count = events.count() + all_events = [] + offset = options["offset"] + + for idx, event in enumerate(events[offset:]): + prefix = f"{idx + offset + 1} of {events_count}" + print(f"{prefix:<10} {event.url}") + # Summary. + if not event.summary and (prompt := Prompt.get_owasp_event_summary()): + event.generate_summary(prompt=prompt) + + # Suggested location. + if not event.suggested_location and (prompt := Prompt.get_owasp_event_suggested_location()): + event.generate_suggested_location() + + # Geo location. + if not event.latitude or not event.longitude: + try: + event.generate_geo_location() + time.sleep(5) + except Exception: + logger.exception( + "Could not get geo data for event", + extra={"url": event.url}, + ) + all_events.append(event) + + Event.bulk_save( + all_events, + fields=("latitude", "longitude", "suggested_location", "summary"), + ) diff --git a/backend/apps/owasp/migrations/0023_event_latitude_event_longitude_and_more.py b/backend/apps/owasp/migrations/0023_event_latitude_event_longitude_and_more.py new file mode 100644 index 0000000000..17e5858752 --- /dev/null +++ b/backend/apps/owasp/migrations/0023_event_latitude_event_longitude_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 5.1.7 on 2025-03-08 20:29 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('owasp', '0022_sponsor'), + ] + + operations = [ + migrations.AddField( + model_name='event', + name='latitude', + field=models.FloatField(blank=True, null=True, verbose_name='Latitude'), + ), + migrations.AddField( + model_name='event', + name='longitude', + field=models.FloatField(blank=True, null=True, verbose_name='Longitude'), + ), + migrations.AddField( + model_name='event', + name='suggested_location', + field=models.CharField(blank=True, default='', max_length=255, verbose_name='Suggested Location'), + ), + migrations.AddField( + model_name='event', + name='summary', + field=models.TextField(blank=True, default='', verbose_name='Summary'), + ), + ] diff --git a/backend/apps/owasp/migrations/0024_event_country_event_postal_code_event_region.py b/backend/apps/owasp/migrations/0024_event_country_event_postal_code_event_region.py new file mode 100644 index 0000000000..c15eee809d --- /dev/null +++ b/backend/apps/owasp/migrations/0024_event_country_event_postal_code_event_region.py @@ -0,0 +1,28 @@ +# Generated by Django 5.1.7 on 2025-03-09 09:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('owasp', '0023_event_latitude_event_longitude_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='event', + name='country', + field=models.CharField(default='', max_length=50, verbose_name='Country'), + ), + migrations.AddField( + model_name='event', + name='postal_code', + field=models.CharField(blank=True, default='', max_length=15, verbose_name='Postal code'), + ), + migrations.AddField( + model_name='event', + name='region', + field=models.CharField(default='', max_length=50, verbose_name='Region'), + ), + ] diff --git a/backend/apps/owasp/models/event.py b/backend/apps/owasp/models/event.py index e5876de66f..1ea31e0f27 100644 --- a/backend/apps/owasp/models/event.py +++ b/backend/apps/owasp/models/event.py @@ -4,8 +4,11 @@ from django.db import models from django.utils import timezone +from apps.common.geocoding import get_location_coordinates from apps.common.models import BulkSaveModel, TimestampedModel -from apps.common.utils import slugify +from apps.common.open_ai import OpenAi +from apps.common.utils import join_values, slugify +from apps.core.models.prompt import Prompt from apps.github.utils import normalize_url @@ -30,12 +33,25 @@ class Category(models.TextChoices): choices=Category.choices, default=Category.OTHER, ) - + country = models.CharField(verbose_name="Country", max_length=50, default="") end_date = models.DateField(verbose_name="End Date", null=True, blank=True) + description = models.TextField(verbose_name="Description", default="", blank=True) key = models.CharField(verbose_name="Key", max_length=100, unique=True) + latitude = models.FloatField(verbose_name="Latitude", null=True, blank=True) + longitude = models.FloatField(verbose_name="Longitude", null=True, blank=True) name = models.CharField(verbose_name="Name", max_length=100) - description = models.TextField(verbose_name="Description", default="", blank=True) + postal_code = models.CharField( + verbose_name="Postal code", + max_length=15, + default="", + blank=True, + ) + region = models.CharField(verbose_name="Region", max_length=50, default="") start_date = models.DateField(verbose_name="Start Date") + summary = models.TextField(verbose_name="Summary", blank=True, default="") + suggested_location = models.CharField( + verbose_name="Suggested Location", max_length=255, blank=True, default="" + ) url = models.URLField(verbose_name="URL", default="", blank=True) def __str__(self): @@ -130,3 +146,48 @@ def from_dict(self, category, data): for key, value in fields.items(): setattr(self, key, value) + + def generate_summary(self, prompt): + """Generate a summary for the event using OpenAI.""" + if not prompt: + return "" + open_ai = OpenAi() + open_ai.set_input(self.description) + open_ai.set_max_tokens(100).set_prompt(prompt) + summary = open_ai.complete() + self.summary = summary if summary and summary != "None" else "" + return self.summary + + def get_geo_string(self, include_name=True): + """Return geo string.""" + return join_values( + ( + self.name.replace("OWASP", "").strip() if include_name else "", + self.country, + self.postal_code, + ), + delimiter=", ", + ) + + def generate_geo_location(self): + """Add latitude and longitude data.""" + location = None + if self.suggested_location and self.suggested_location != "None": + location = get_location_coordinates(self.suggested_location) + if location is None: + location = get_location_coordinates(self.get_geo_string()) + if location: + self.latitude = location.latitude + self.longitude = location.longitude + + def generate_suggested_location(self, open_ai=None, max_tokens=100): + """Generate project summary.""" + if not (prompt := Prompt.get_owasp_event_suggested_location()): + return + open_ai = open_ai or OpenAi() + open_ai.set_input(self.get_geo_string()) + open_ai.set_max_tokens(max_tokens).set_prompt(prompt) + suggested_location = open_ai.complete() + self.suggested_location = ( + suggested_location if suggested_location and suggested_location != "None" else "" + ) diff --git a/frontend/__tests__/e2e/pages/Home.spec.ts b/frontend/__tests__/e2e/pages/Home.spec.ts index afaef3339d..36990f84de 100644 --- a/frontend/__tests__/e2e/pages/Home.spec.ts +++ b/frontend/__tests__/e2e/pages/Home.spec.ts @@ -70,9 +70,9 @@ test.describe('Home Page', () => { test('should have upcoming events', async ({ page }) => { await expect(page.getByRole('heading', { name: 'Upcoming Events' })).toBeVisible() - await expect(page.getByRole('link', { name: 'Event 1' })).toBeVisible() + await expect(page.getByRole('button', { name: 'Event 1' })).toBeVisible() await expect(page.getByText('Feb 27,')).toBeVisible() await expect(page.getByText('Feb 28,')).toBeVisible() - await page.getByRole('link', { name: 'Event 1' }).click() + await page.getByRole('button', { name: 'Event 1' }).click() }) }) diff --git a/frontend/__tests__/unit/pages/Modal.test.tsx b/frontend/__tests__/unit/pages/Modal.test.tsx index a41411feb4..c8e2208f2e 100644 --- a/frontend/__tests__/unit/pages/Modal.test.tsx +++ b/frontend/__tests__/unit/pages/Modal.test.tsx @@ -38,6 +38,7 @@ describe('Dialog Component', () => { url: 'https://example.com/issue/123', }, children: undefined as React.ReactNode | undefined, + entityType: 'issue', } const renderModal = (props = defaultProps) => { diff --git a/frontend/src/api/queries/homeQueries.ts b/frontend/src/api/queries/homeQueries.ts index 0c26360fe3..b6a68beb3c 100644 --- a/frontend/src/api/queries/homeQueries.ts +++ b/frontend/src/api/queries/homeQueries.ts @@ -64,9 +64,15 @@ export const GET_MAIN_PAGE_DATA = gql` } upcomingEvents(limit: 6) { category + country + description endDate + key name + postalCode + region startDate + summary url } } diff --git a/frontend/src/components/Modal.tsx b/frontend/src/components/Modal.tsx index 94b41a1f55..f37745ed52 100644 --- a/frontend/src/components/Modal.tsx +++ b/frontend/src/components/Modal.tsx @@ -23,6 +23,7 @@ const Modal: React.FC = ({ onClose, button, children, + entityType, }: ModalProps) => { return ( = ({ {title} - The issue summary and the recommended steps to address it have been generated by AI + The {entityType} summary and the recommended steps to address it have been generated by AI = ({ /> - Issue Summary + + {entityType[0].toUpperCase() + entityType.substring(1)} Summary + {hint && ( diff --git a/frontend/src/pages/Contribute.tsx b/frontend/src/pages/Contribute.tsx index 4c053610a6..0ab0ec97e3 100644 --- a/frontend/src/pages/Contribute.tsx +++ b/frontend/src/pages/Contribute.tsx @@ -64,6 +64,7 @@ const ContributePage = () => { summary={issue.summary} hint={issue.hint} button={viewIssueButton} + entityType="issue" > ) diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index 41aee67a00..0bbaa14f99 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -24,6 +24,7 @@ import ChapterMap from 'components/ChapterMap' import ItemCardList from 'components/ItemCardList' import LoadingSpinner from 'components/LoadingSpinner' import MovingLogos from 'components/LogoCarousel' +import Modal from 'components/Modal' import MultiSearchBar from 'components/MultiSearch' import SecondaryCard from 'components/SecondaryCard' import TopContributors from 'components/ToggleContributors' @@ -33,6 +34,7 @@ export default function Home() { const [data, setData] = useState(null) const { data: graphQLData, error: graphQLRequestError } = useQuery(GET_MAIN_PAGE_DATA) const [geoLocData, setGeoLocData] = useState([]) + const [modalOpenIndex, setModalOpenIndex] = useState(null) useEffect(() => { if (graphQLData) { @@ -131,28 +133,35 @@ export default function Home() {
- {data.upcomingEvents.map((event: EventType) => ( -
-

- ( +

-
-
- - - {event.endDate && event.startDate != event.endDate - ? `${formatDate(event.startDate)} - ${formatDate(event.endDate)}` - : formatDate(event.startDate)} - +

{event.name}

+ +
+
+ + + {event.endDate && event.startDate != event.endDate + ? `${formatDate(event.startDate)} - ${formatDate(event.endDate)}` + : formatDate(event.startDate)} + +
+ setModalOpenIndex(null)} + title={event.name} + summary={event.summary} + button={{ label: 'View Event', url: event.url }} + entityType="event" + >
))}
diff --git a/frontend/src/types/event.ts b/frontend/src/types/event.ts index c540720290..19978b3536 100644 --- a/frontend/src/types/event.ts +++ b/frontend/src/types/event.ts @@ -5,4 +5,5 @@ export type EventType = { name: string startDate: string url: string + summary?: string } diff --git a/frontend/src/types/modal.ts b/frontend/src/types/modal.ts index 089e479d22..1d8f5aa101 100644 --- a/frontend/src/types/modal.ts +++ b/frontend/src/types/modal.ts @@ -9,4 +9,5 @@ export interface ModalProps { onClose: () => void button: ButtonType children?: React.ReactNode + entityType: string } From 6b528a641d2de2917069028853bac6652621b433 Mon Sep 17 00:00:00 2001 From: Samyak Jain Date: Sun, 9 Mar 2025 23:06:39 +0530 Subject: [PATCH 2/6] Fix formatting error caused by merge conflict. --- .../commands/owasp_enrich_events.py | 4 ++- ...event_latitude_event_longitude_and_more.py | 29 ++++++++++--------- ..._country_event_postal_code_event_region.py | 23 ++++++++------- 3 files changed, 30 insertions(+), 26 deletions(-) mode change 100644 => 100755 backend/apps/owasp/migrations/0023_event_latitude_event_longitude_and_more.py mode change 100644 => 100755 backend/apps/owasp/migrations/0024_event_country_event_postal_code_event_region.py diff --git a/backend/apps/owasp/management/commands/owasp_enrich_events.py b/backend/apps/owasp/management/commands/owasp_enrich_events.py index 6b93b4ae61..33f64f4464 100644 --- a/backend/apps/owasp/management/commands/owasp_enrich_events.py +++ b/backend/apps/owasp/management/commands/owasp_enrich_events.py @@ -31,7 +31,9 @@ def handle(self, *args, **options): event.generate_summary(prompt=prompt) # Suggested location. - if not event.suggested_location and (prompt := Prompt.get_owasp_event_suggested_location()): + if not event.suggested_location and ( + prompt := Prompt.get_owasp_event_suggested_location() + ): event.generate_suggested_location() # Geo location. diff --git a/backend/apps/owasp/migrations/0023_event_latitude_event_longitude_and_more.py b/backend/apps/owasp/migrations/0023_event_latitude_event_longitude_and_more.py old mode 100644 new mode 100755 index 17e5858752..ec2e31c4fd --- a/backend/apps/owasp/migrations/0023_event_latitude_event_longitude_and_more.py +++ b/backend/apps/owasp/migrations/0023_event_latitude_event_longitude_and_more.py @@ -4,30 +4,31 @@ class Migration(migrations.Migration): - dependencies = [ - ('owasp', '0022_sponsor'), + ("owasp", "0022_sponsor"), ] operations = [ migrations.AddField( - model_name='event', - name='latitude', - field=models.FloatField(blank=True, null=True, verbose_name='Latitude'), + model_name="event", + name="latitude", + field=models.FloatField(blank=True, null=True, verbose_name="Latitude"), ), migrations.AddField( - model_name='event', - name='longitude', - field=models.FloatField(blank=True, null=True, verbose_name='Longitude'), + model_name="event", + name="longitude", + field=models.FloatField(blank=True, null=True, verbose_name="Longitude"), ), migrations.AddField( - model_name='event', - name='suggested_location', - field=models.CharField(blank=True, default='', max_length=255, verbose_name='Suggested Location'), + model_name="event", + name="suggested_location", + field=models.CharField( + blank=True, default="", max_length=255, verbose_name="Suggested Location" + ), ), migrations.AddField( - model_name='event', - name='summary', - field=models.TextField(blank=True, default='', verbose_name='Summary'), + model_name="event", + name="summary", + field=models.TextField(blank=True, default="", verbose_name="Summary"), ), ] diff --git a/backend/apps/owasp/migrations/0024_event_country_event_postal_code_event_region.py b/backend/apps/owasp/migrations/0024_event_country_event_postal_code_event_region.py old mode 100644 new mode 100755 index c15eee809d..50ce349cbc --- a/backend/apps/owasp/migrations/0024_event_country_event_postal_code_event_region.py +++ b/backend/apps/owasp/migrations/0024_event_country_event_postal_code_event_region.py @@ -4,25 +4,26 @@ class Migration(migrations.Migration): - dependencies = [ - ('owasp', '0023_event_latitude_event_longitude_and_more'), + ("owasp", "0023_event_latitude_event_longitude_and_more"), ] operations = [ migrations.AddField( - model_name='event', - name='country', - field=models.CharField(default='', max_length=50, verbose_name='Country'), + model_name="event", + name="country", + field=models.CharField(default="", max_length=50, verbose_name="Country"), ), migrations.AddField( - model_name='event', - name='postal_code', - field=models.CharField(blank=True, default='', max_length=15, verbose_name='Postal code'), + model_name="event", + name="postal_code", + field=models.CharField( + blank=True, default="", max_length=15, verbose_name="Postal code" + ), ), migrations.AddField( - model_name='event', - name='region', - field=models.CharField(default='', max_length=50, verbose_name='Region'), + model_name="event", + name="region", + field=models.CharField(default="", max_length=50, verbose_name="Region"), ), ] From a399d509f3e067288328b3ace72bb6c70e0b7c88 Mon Sep 17 00:00:00 2001 From: Samyak Jain Date: Mon, 10 Mar 2025 00:18:26 +0530 Subject: [PATCH 3/6] Fixed permissions --- .../migrations/0023_event_latitude_event_longitude_and_more.py | 0 .../0024_event_country_event_postal_code_event_region.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 backend/apps/owasp/migrations/0023_event_latitude_event_longitude_and_more.py mode change 100755 => 100644 backend/apps/owasp/migrations/0024_event_country_event_postal_code_event_region.py diff --git a/backend/apps/owasp/migrations/0023_event_latitude_event_longitude_and_more.py b/backend/apps/owasp/migrations/0023_event_latitude_event_longitude_and_more.py old mode 100755 new mode 100644 diff --git a/backend/apps/owasp/migrations/0024_event_country_event_postal_code_event_region.py b/backend/apps/owasp/migrations/0024_event_country_event_postal_code_event_region.py old mode 100755 new mode 100644 From 93a577d4fd010afb789a62c17de995eb82f313ea Mon Sep 17 00:00:00 2001 From: Arkadii Yakovets Date: Sun, 9 Mar 2025 20:04:25 -0700 Subject: [PATCH 4/6] Update code --- backend/apps/owasp/admin.py | 5 ++- backend/apps/owasp/graphql/nodes/event.py | 4 +-- .../commands/owasp_enrich_events.py | 3 +- ...untry_remove_event_postal_code_and_more.py | 24 +++++++++++++ backend/apps/owasp/models/event.py | 36 ++++++++++--------- frontend/src/api/queries/homeQueries.ts | 5 +-- 6 files changed, 50 insertions(+), 27 deletions(-) create mode 100644 backend/apps/owasp/migrations/0025_remove_event_country_remove_event_postal_code_and_more.py diff --git a/backend/apps/owasp/admin.py b/backend/apps/owasp/admin.py index c6aabd1c20..244c9c9967 100644 --- a/backend/apps/owasp/admin.py +++ b/backend/apps/owasp/admin.py @@ -66,7 +66,10 @@ class CommitteeAdmin(admin.ModelAdmin): class EventAdmin(admin.ModelAdmin): - list_display = ("name",) + list_display = ( + "name", + "suggested_location", + ) search_fields = ("name",) diff --git a/backend/apps/owasp/graphql/nodes/event.py b/backend/apps/owasp/graphql/nodes/event.py index b74f0b58af..db6b9e6c94 100644 --- a/backend/apps/owasp/graphql/nodes/event.py +++ b/backend/apps/owasp/graphql/nodes/event.py @@ -11,14 +11,12 @@ class Meta: model = Event fields = ( "category", - "country", "end_date", "description", "key", "name", - "postal_code", - "region", "start_date", + "suggested_location", "summary", "url", ) diff --git a/backend/apps/owasp/management/commands/owasp_enrich_events.py b/backend/apps/owasp/management/commands/owasp_enrich_events.py index 33f64f4464..46454b5a0c 100644 --- a/backend/apps/owasp/management/commands/owasp_enrich_events.py +++ b/backend/apps/owasp/management/commands/owasp_enrich_events.py @@ -19,12 +19,11 @@ def add_arguments(self, parser): def handle(self, *args, **options): events = Event.objects.order_by("id") - events_count = events.count() all_events = [] offset = options["offset"] for idx, event in enumerate(events[offset:]): - prefix = f"{idx + offset + 1} of {events_count}" + prefix = f"{idx + offset + 1} of {events.count()}" print(f"{prefix:<10} {event.url}") # Summary. if not event.summary and (prompt := Prompt.get_owasp_event_summary()): diff --git a/backend/apps/owasp/migrations/0025_remove_event_country_remove_event_postal_code_and_more.py b/backend/apps/owasp/migrations/0025_remove_event_country_remove_event_postal_code_and_more.py new file mode 100644 index 0000000000..8d6dd06442 --- /dev/null +++ b/backend/apps/owasp/migrations/0025_remove_event_country_remove_event_postal_code_and_more.py @@ -0,0 +1,24 @@ +# Generated by Django 5.1.7 on 2025-03-10 02:28 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("owasp", "0024_event_country_event_postal_code_event_region"), + ] + + operations = [ + migrations.RemoveField( + model_name="event", + name="country", + ), + migrations.RemoveField( + model_name="event", + name="postal_code", + ), + migrations.RemoveField( + model_name="event", + name="region", + ), + ] diff --git a/backend/apps/owasp/models/event.py b/backend/apps/owasp/models/event.py index 1ea31e0f27..0b431a25eb 100644 --- a/backend/apps/owasp/models/event.py +++ b/backend/apps/owasp/models/event.py @@ -4,6 +4,7 @@ from django.db import models from django.utils import timezone +from apps.common.constants import NL from apps.common.geocoding import get_location_coordinates from apps.common.models import BulkSaveModel, TimestampedModel from apps.common.open_ai import OpenAi @@ -33,26 +34,18 @@ class Category(models.TextChoices): choices=Category.choices, default=Category.OTHER, ) - country = models.CharField(verbose_name="Country", max_length=50, default="") + name = models.CharField(verbose_name="Name", max_length=100) + start_date = models.DateField(verbose_name="Start Date") end_date = models.DateField(verbose_name="End Date", null=True, blank=True) description = models.TextField(verbose_name="Description", default="", blank=True) key = models.CharField(verbose_name="Key", max_length=100, unique=True) - latitude = models.FloatField(verbose_name="Latitude", null=True, blank=True) - longitude = models.FloatField(verbose_name="Longitude", null=True, blank=True) - name = models.CharField(verbose_name="Name", max_length=100) - postal_code = models.CharField( - verbose_name="Postal code", - max_length=15, - default="", - blank=True, - ) - region = models.CharField(verbose_name="Region", max_length=50, default="") - start_date = models.DateField(verbose_name="Start Date") summary = models.TextField(verbose_name="Summary", blank=True, default="") suggested_location = models.CharField( verbose_name="Suggested Location", max_length=255, blank=True, default="" ) url = models.URLField(verbose_name="URL", default="", blank=True) + latitude = models.FloatField(verbose_name="Latitude", null=True, blank=True) + longitude = models.FloatField(verbose_name="Longitude", null=True, blank=True) def __str__(self): """Event human readable representation.""" @@ -152,7 +145,16 @@ def generate_summary(self, prompt): if not prompt: return "" open_ai = OpenAi() - open_ai.set_input(self.description) + open_ai.set_input( + join_values( + ( + f"Name: {self.name}", + f"Description: {self.description}", + f"Dates: {self.start_date} - {self.end_date}", + ), + delimiter=NL, + ) + ) open_ai.set_max_tokens(100).set_prompt(prompt) summary = open_ai.complete() self.summary = summary if summary and summary != "None" else "" @@ -162,11 +164,11 @@ def get_geo_string(self, include_name=True): """Return geo string.""" return join_values( ( - self.name.replace("OWASP", "").strip() if include_name else "", - self.country, - self.postal_code, + f"Name: {self.name}", + f"Description: {self.description}", + f"Summary: {self.summary}", ), - delimiter=", ", + delimiter=NL, ) def generate_geo_location(self): diff --git a/frontend/src/api/queries/homeQueries.ts b/frontend/src/api/queries/homeQueries.ts index b6a68beb3c..c03a3d01df 100644 --- a/frontend/src/api/queries/homeQueries.ts +++ b/frontend/src/api/queries/homeQueries.ts @@ -64,15 +64,12 @@ export const GET_MAIN_PAGE_DATA = gql` } upcomingEvents(limit: 6) { category - country - description endDate key name - postalCode - region startDate summary + suggestedLocation url } } From a14d0a39f0b71d4f8a87d7672851197d3967fdd8 Mon Sep 17 00:00:00 2001 From: Samyak Jain Date: Wed, 12 Mar 2025 00:08:58 +0530 Subject: [PATCH 5/6] Added few changes --- backend/Makefile | 1 + backend/apps/owasp/models/event.py | 20 ++++++++++++++------ frontend/__tests__/unit/pages/Modal.test.tsx | 3 ++- frontend/src/components/Modal.tsx | 10 +++------- frontend/src/pages/Contribute.tsx | 2 +- frontend/src/pages/Home.tsx | 12 ++++++++---- frontend/src/types/event.ts | 1 + frontend/src/types/modal.ts | 2 +- 8 files changed, 31 insertions(+), 20 deletions(-) diff --git a/backend/Makefile b/backend/Makefile index 6d023cb50e..3d901574ba 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -22,6 +22,7 @@ enrich-data: \ github-enrich-issues \ owasp-enrich-chapters \ owasp-enrich-committees \ + owasp-enrich-events \ owasp-enrich-projects generate-sitemap: diff --git a/backend/apps/owasp/models/event.py b/backend/apps/owasp/models/event.py index 0b431a25eb..12676f0618 100644 --- a/backend/apps/owasp/models/event.py +++ b/backend/apps/owasp/models/event.py @@ -145,6 +145,8 @@ def generate_summary(self, prompt): if not prompt: return "" open_ai = OpenAi() + if not self.description: + return "" open_ai.set_input( join_values( ( @@ -156,8 +158,11 @@ def generate_summary(self, prompt): ) ) open_ai.set_max_tokens(100).set_prompt(prompt) - summary = open_ai.complete() - self.summary = summary if summary and summary != "None" else "" + try: + summary = open_ai.complete() + self.summary = summary if summary and summary != "None" else "" + except (ValueError, TypeError): + self.summary = "" return self.summary def get_geo_string(self, include_name=True): @@ -189,7 +194,10 @@ def generate_suggested_location(self, open_ai=None, max_tokens=100): open_ai = open_ai or OpenAi() open_ai.set_input(self.get_geo_string()) open_ai.set_max_tokens(max_tokens).set_prompt(prompt) - suggested_location = open_ai.complete() - self.suggested_location = ( - suggested_location if suggested_location and suggested_location != "None" else "" - ) + try: + suggested_location = open_ai.complete() + self.suggested_location = ( + suggested_location if suggested_location and suggested_location != "None" else "" + ) + except (ValueError, TypeError): + self.suggested_location = "" diff --git a/frontend/__tests__/unit/pages/Modal.test.tsx b/frontend/__tests__/unit/pages/Modal.test.tsx index c8e2208f2e..e6118736a6 100644 --- a/frontend/__tests__/unit/pages/Modal.test.tsx +++ b/frontend/__tests__/unit/pages/Modal.test.tsx @@ -38,7 +38,8 @@ describe('Dialog Component', () => { url: 'https://example.com/issue/123', }, children: undefined as React.ReactNode | undefined, - entityType: 'issue', + description: + 'The issue summary and the recommended steps to address it have been generated by AI', } const renderModal = (props = defaultProps) => { diff --git a/frontend/src/components/Modal.tsx b/frontend/src/components/Modal.tsx index f37745ed52..432bbe8a77 100644 --- a/frontend/src/components/Modal.tsx +++ b/frontend/src/components/Modal.tsx @@ -23,7 +23,7 @@ const Modal: React.FC = ({ onClose, button, children, - entityType, + description, }: ModalProps) => { return ( = ({ {title} - - The {entityType} summary and the recommended steps to address it have been generated by AI - + {description} - - {entityType[0].toUpperCase() + entityType.substring(1)} Summary - + Summary {hint && ( diff --git a/frontend/src/pages/Contribute.tsx b/frontend/src/pages/Contribute.tsx index 0ab0ec97e3..11230ceed1 100644 --- a/frontend/src/pages/Contribute.tsx +++ b/frontend/src/pages/Contribute.tsx @@ -64,7 +64,7 @@ const ContributePage = () => { summary={issue.summary} hint={issue.hint} button={viewIssueButton} - entityType="issue" + description="The issue summary and the recommended steps to address it have been generated by AI" > ) diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index 71e0e05123..0ec353a5ba 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -135,8 +135,8 @@ export default function Home() {
{data.upcomingEvents.map((event: EventType, index: number) => ( -
-
+
+
+
+ + {event.suggestedLocation} +
setModalOpenIndex(null)} title={event.name} summary={event.summary} button={{ label: 'View Event', url: event.url }} - entityType="event" + description="The event summary has been generated by AI" >
))} diff --git a/frontend/src/types/event.ts b/frontend/src/types/event.ts index 19978b3536..028e87e312 100644 --- a/frontend/src/types/event.ts +++ b/frontend/src/types/event.ts @@ -6,4 +6,5 @@ export type EventType = { startDate: string url: string summary?: string + suggestedLocation?: string } diff --git a/frontend/src/types/modal.ts b/frontend/src/types/modal.ts index 1d8f5aa101..5c6fb1e08b 100644 --- a/frontend/src/types/modal.ts +++ b/frontend/src/types/modal.ts @@ -9,5 +9,5 @@ export interface ModalProps { onClose: () => void button: ButtonType children?: React.ReactNode - entityType: string + description: string } From 4eb7b29bf6f7c233eddfa4c74b7b4a0b4faf7890 Mon Sep 17 00:00:00 2001 From: Arkadii Yakovets Date: Tue, 11 Mar 2025 17:04:49 -0700 Subject: [PATCH 6/6] Update code --- .../commands/owasp_enrich_events.py | 4 +- backend/apps/owasp/models/event.py | 77 ++++++++----------- 2 files changed, 34 insertions(+), 47 deletions(-) diff --git a/backend/apps/owasp/management/commands/owasp_enrich_events.py b/backend/apps/owasp/management/commands/owasp_enrich_events.py index 46454b5a0c..4c61bf10ff 100644 --- a/backend/apps/owasp/management/commands/owasp_enrich_events.py +++ b/backend/apps/owasp/management/commands/owasp_enrich_events.py @@ -27,13 +27,13 @@ def handle(self, *args, **options): print(f"{prefix:<10} {event.url}") # Summary. if not event.summary and (prompt := Prompt.get_owasp_event_summary()): - event.generate_summary(prompt=prompt) + event.generate_summary(prompt) # Suggested location. if not event.suggested_location and ( prompt := Prompt.get_owasp_event_suggested_location() ): - event.generate_suggested_location() + event.generate_suggested_location(prompt) # Geo location. if not event.latitude or not event.longitude: diff --git a/backend/apps/owasp/models/event.py b/backend/apps/owasp/models/event.py index 12676f0618..35aa6353b8 100644 --- a/backend/apps/owasp/models/event.py +++ b/backend/apps/owasp/models/event.py @@ -9,7 +9,6 @@ from apps.common.models import BulkSaveModel, TimestampedModel from apps.common.open_ai import OpenAi from apps.common.utils import join_values, slugify -from apps.core.models.prompt import Prompt from apps.github.utils import normalize_url @@ -140,60 +139,22 @@ def from_dict(self, category, data): for key, value in fields.items(): setattr(self, key, value) - def generate_summary(self, prompt): - """Generate a summary for the event using OpenAI.""" - if not prompt: - return "" - open_ai = OpenAi() - if not self.description: - return "" - open_ai.set_input( - join_values( - ( - f"Name: {self.name}", - f"Description: {self.description}", - f"Dates: {self.start_date} - {self.end_date}", - ), - delimiter=NL, - ) - ) - open_ai.set_max_tokens(100).set_prompt(prompt) - try: - summary = open_ai.complete() - self.summary = summary if summary and summary != "None" else "" - except (ValueError, TypeError): - self.summary = "" - return self.summary - - def get_geo_string(self, include_name=True): - """Return geo string.""" - return join_values( - ( - f"Name: {self.name}", - f"Description: {self.description}", - f"Summary: {self.summary}", - ), - delimiter=NL, - ) - def generate_geo_location(self): """Add latitude and longitude data.""" location = None if self.suggested_location and self.suggested_location != "None": location = get_location_coordinates(self.suggested_location) if location is None: - location = get_location_coordinates(self.get_geo_string()) + location = get_location_coordinates(self.get_context()) if location: self.latitude = location.latitude self.longitude = location.longitude - def generate_suggested_location(self, open_ai=None, max_tokens=100): - """Generate project summary.""" - if not (prompt := Prompt.get_owasp_event_suggested_location()): - return - open_ai = open_ai or OpenAi() - open_ai.set_input(self.get_geo_string()) - open_ai.set_max_tokens(max_tokens).set_prompt(prompt) + def generate_suggested_location(self, prompt): + """Generate a suggested location for the event.""" + open_ai = OpenAi() + open_ai.set_input(self.get_context()) + open_ai.set_max_tokens(100).set_prompt(prompt) try: suggested_location = open_ai.complete() self.suggested_location = ( @@ -201,3 +162,29 @@ def generate_suggested_location(self, open_ai=None, max_tokens=100): ) except (ValueError, TypeError): self.suggested_location = "" + + def generate_summary(self, prompt): + """Generate a summary for the event.""" + open_ai = OpenAi() + open_ai.set_input(self.get_context(include_dates=True)) + open_ai.set_max_tokens(100).set_prompt(prompt) + try: + summary = open_ai.complete() + self.summary = summary if summary and summary != "None" else "" + except (ValueError, TypeError): + self.summary = "" + + def get_context(self, include_dates=False): + """Return geo string.""" + context = [ + f"Name: {self.name}", + f"Description: {self.description}", + f"Summary: {self.summary}", + ] + if include_dates: + context.append(f"Dates: {self.start_date} - {self.end_date}") + + return join_values( + context, + delimiter=NL, + )