Skip to content

Commit 2c91c1a

Browse files
samyak003arkid15r
andauthored
Added AI processing to events (#1066)
* Added AI processing to events * Fix formatting error caused by merge conflict. * Fixed permissions * Update code * Added few changes * Update code --------- Co-authored-by: Arkadii Yakovets <arkadii.yakovets@owasp.org>
1 parent 8cdfa74 commit 2c91c1a

19 files changed

Lines changed: 280 additions & 40 deletions

backend/Makefile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ enrich-data: \
2222
github-enrich-issues \
2323
owasp-enrich-chapters \
2424
owasp-enrich-committees \
25+
owasp-enrich-events \
2526
owasp-enrich-projects
2627

2728
generate-sitemap:
@@ -70,6 +71,10 @@ owasp-enrich-committees:
7071
@echo "Enriching OWASP committees"
7172
@CMD="python manage.py owasp_enrich_committees" $(MAKE) exec-backend-command
7273

74+
owasp-enrich-events:
75+
@echo "Enriching OWASP events"
76+
@CMD="python manage.py owasp_enrich_events" $(MAKE) exec-backend-command
77+
7378
owasp-enrich-projects:
7479
@echo "Enriching OWASP projects"
7580
@CMD="python manage.py owasp_enrich_projects" $(MAKE) exec-backend-command

backend/apps/core/models/prompt.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,16 @@ def get_owasp_committee_summary():
7171
"""Return OWASP committee summary prompt."""
7272
return Prompt.get_text("owasp-committee-summary")
7373

74+
@staticmethod
75+
def get_owasp_event_suggested_location():
76+
"""Return OWASP event suggested location prompt."""
77+
return Prompt.get_text("owasp-event-suggested-location")
78+
79+
@staticmethod
80+
def get_owasp_event_summary():
81+
"""Return OWASP event summary prompt."""
82+
return Prompt.get_text("owasp-event-summary")
83+
7484
@staticmethod
7585
def get_owasp_project_summary():
7686
"""Return OWASP project summary prompt."""

backend/apps/owasp/admin.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,10 @@ class CommitteeAdmin(admin.ModelAdmin):
6666

6767

6868
class EventAdmin(admin.ModelAdmin):
69-
list_display = ("name",)
69+
list_display = (
70+
"name",
71+
"suggested_location",
72+
)
7073
search_fields = ("name",)
7174

7275

backend/apps/owasp/graphql/nodes/event.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,7 @@ class Meta:
1616
"key",
1717
"name",
1818
"start_date",
19+
"suggested_location",
20+
"summary",
1921
"url",
2022
)
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"""A command to enrich events with extra data."""
2+
3+
import logging
4+
import time
5+
6+
from django.core.management.base import BaseCommand
7+
8+
from apps.core.models.prompt import Prompt
9+
from apps.owasp.models.event import Event
10+
11+
logger = logging.getLogger(__name__)
12+
13+
14+
class Command(BaseCommand):
15+
help = "Enrich events with extra data."
16+
17+
def add_arguments(self, parser):
18+
parser.add_argument("--offset", default=0, required=False, type=int)
19+
20+
def handle(self, *args, **options):
21+
events = Event.objects.order_by("id")
22+
all_events = []
23+
offset = options["offset"]
24+
25+
for idx, event in enumerate(events[offset:]):
26+
prefix = f"{idx + offset + 1} of {events.count()}"
27+
print(f"{prefix:<10} {event.url}")
28+
# Summary.
29+
if not event.summary and (prompt := Prompt.get_owasp_event_summary()):
30+
event.generate_summary(prompt)
31+
32+
# Suggested location.
33+
if not event.suggested_location and (
34+
prompt := Prompt.get_owasp_event_suggested_location()
35+
):
36+
event.generate_suggested_location(prompt)
37+
38+
# Geo location.
39+
if not event.latitude or not event.longitude:
40+
try:
41+
event.generate_geo_location()
42+
time.sleep(5)
43+
except Exception:
44+
logger.exception(
45+
"Could not get geo data for event",
46+
extra={"url": event.url},
47+
)
48+
all_events.append(event)
49+
50+
Event.bulk_save(
51+
all_events,
52+
fields=("latitude", "longitude", "suggested_location", "summary"),
53+
)
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Generated by Django 5.1.7 on 2025-03-08 20:29
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
dependencies = [
8+
("owasp", "0022_sponsor"),
9+
]
10+
11+
operations = [
12+
migrations.AddField(
13+
model_name="event",
14+
name="latitude",
15+
field=models.FloatField(blank=True, null=True, verbose_name="Latitude"),
16+
),
17+
migrations.AddField(
18+
model_name="event",
19+
name="longitude",
20+
field=models.FloatField(blank=True, null=True, verbose_name="Longitude"),
21+
),
22+
migrations.AddField(
23+
model_name="event",
24+
name="suggested_location",
25+
field=models.CharField(
26+
blank=True, default="", max_length=255, verbose_name="Suggested Location"
27+
),
28+
),
29+
migrations.AddField(
30+
model_name="event",
31+
name="summary",
32+
field=models.TextField(blank=True, default="", verbose_name="Summary"),
33+
),
34+
]
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Generated by Django 5.1.7 on 2025-03-09 09:12
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
dependencies = [
8+
("owasp", "0023_event_latitude_event_longitude_and_more"),
9+
]
10+
11+
operations = [
12+
migrations.AddField(
13+
model_name="event",
14+
name="country",
15+
field=models.CharField(default="", max_length=50, verbose_name="Country"),
16+
),
17+
migrations.AddField(
18+
model_name="event",
19+
name="postal_code",
20+
field=models.CharField(
21+
blank=True, default="", max_length=15, verbose_name="Postal code"
22+
),
23+
),
24+
migrations.AddField(
25+
model_name="event",
26+
name="region",
27+
field=models.CharField(default="", max_length=50, verbose_name="Region"),
28+
),
29+
]
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Generated by Django 5.1.7 on 2025-03-10 02:28
2+
3+
from django.db import migrations
4+
5+
6+
class Migration(migrations.Migration):
7+
dependencies = [
8+
("owasp", "0024_event_country_event_postal_code_event_region"),
9+
]
10+
11+
operations = [
12+
migrations.RemoveField(
13+
model_name="event",
14+
name="country",
15+
),
16+
migrations.RemoveField(
17+
model_name="event",
18+
name="postal_code",
19+
),
20+
migrations.RemoveField(
21+
model_name="event",
22+
name="region",
23+
),
24+
]

backend/apps/owasp/models/event.py

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@
44
from django.db import models
55
from django.utils import timezone
66

7+
from apps.common.constants import NL
8+
from apps.common.geocoding import get_location_coordinates
79
from apps.common.models import BulkSaveModel, TimestampedModel
8-
from apps.common.utils import slugify
10+
from apps.common.open_ai import OpenAi
11+
from apps.common.utils import join_values, slugify
912
from apps.github.utils import normalize_url
1013

1114

@@ -30,13 +33,18 @@ class Category(models.TextChoices):
3033
choices=Category.choices,
3134
default=Category.OTHER,
3235
)
33-
34-
end_date = models.DateField(verbose_name="End Date", null=True, blank=True)
35-
key = models.CharField(verbose_name="Key", max_length=100, unique=True)
3636
name = models.CharField(verbose_name="Name", max_length=100)
37-
description = models.TextField(verbose_name="Description", default="", blank=True)
3837
start_date = models.DateField(verbose_name="Start Date")
38+
end_date = models.DateField(verbose_name="End Date", null=True, blank=True)
39+
description = models.TextField(verbose_name="Description", default="", blank=True)
40+
key = models.CharField(verbose_name="Key", max_length=100, unique=True)
41+
summary = models.TextField(verbose_name="Summary", blank=True, default="")
42+
suggested_location = models.CharField(
43+
verbose_name="Suggested Location", max_length=255, blank=True, default=""
44+
)
3945
url = models.URLField(verbose_name="URL", default="", blank=True)
46+
latitude = models.FloatField(verbose_name="Latitude", null=True, blank=True)
47+
longitude = models.FloatField(verbose_name="Longitude", null=True, blank=True)
4048

4149
def __str__(self):
4250
"""Event human readable representation."""
@@ -130,3 +138,53 @@ def from_dict(self, category, data):
130138

131139
for key, value in fields.items():
132140
setattr(self, key, value)
141+
142+
def generate_geo_location(self):
143+
"""Add latitude and longitude data."""
144+
location = None
145+
if self.suggested_location and self.suggested_location != "None":
146+
location = get_location_coordinates(self.suggested_location)
147+
if location is None:
148+
location = get_location_coordinates(self.get_context())
149+
if location:
150+
self.latitude = location.latitude
151+
self.longitude = location.longitude
152+
153+
def generate_suggested_location(self, prompt):
154+
"""Generate a suggested location for the event."""
155+
open_ai = OpenAi()
156+
open_ai.set_input(self.get_context())
157+
open_ai.set_max_tokens(100).set_prompt(prompt)
158+
try:
159+
suggested_location = open_ai.complete()
160+
self.suggested_location = (
161+
suggested_location if suggested_location and suggested_location != "None" else ""
162+
)
163+
except (ValueError, TypeError):
164+
self.suggested_location = ""
165+
166+
def generate_summary(self, prompt):
167+
"""Generate a summary for the event."""
168+
open_ai = OpenAi()
169+
open_ai.set_input(self.get_context(include_dates=True))
170+
open_ai.set_max_tokens(100).set_prompt(prompt)
171+
try:
172+
summary = open_ai.complete()
173+
self.summary = summary if summary and summary != "None" else ""
174+
except (ValueError, TypeError):
175+
self.summary = ""
176+
177+
def get_context(self, include_dates=False):
178+
"""Return geo string."""
179+
context = [
180+
f"Name: {self.name}",
181+
f"Description: {self.description}",
182+
f"Summary: {self.summary}",
183+
]
184+
if include_dates:
185+
context.append(f"Dates: {self.start_date} - {self.end_date}")
186+
187+
return join_values(
188+
context,
189+
delimiter=NL,
190+
)

frontend/__tests__/e2e/pages/Home.spec.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,17 +61,17 @@ test.describe('Home Page', () => {
6161
test('should be able to join OWASP', async ({ page }) => {
6262
await expect(page.getByRole('heading', { name: 'Ready to Make a Difference?' })).toBeVisible()
6363
await expect(page.getByText('Join OWASP and be part of the')).toBeVisible()
64-
await expect(page.getByRole('link', { name: 'Join OWASP Now' })).toBeVisible()
64+
await expect(page.getByRole('link', { name: 'Join OWASP' })).toBeVisible()
6565
const page1Promise = page.waitForEvent('popup')
66-
await page.getByRole('link', { name: 'Join OWASP Now' }).click()
66+
await page.getByRole('link', { name: 'Join OWASP' }).click()
6767
const page1 = await page1Promise
6868
expect(page1.url()).toBe('https://owasp.glueup.com/organization/6727/memberships/')
6969
})
7070

7171
test('should have upcoming events', async ({ page }) => {
7272
await expect(page.getByRole('heading', { name: 'Upcoming Events' })).toBeVisible()
73-
await expect(page.getByRole('link', { name: 'Event 1' })).toBeVisible()
73+
await expect(page.getByRole('button', { name: 'Event 1' })).toBeVisible()
7474
await expect(page.getByText('Feb 27 — 28, 2025')).toBeVisible()
75-
await page.getByRole('link', { name: 'Event 1' }).click()
75+
await page.getByRole('button', { name: 'Event 1' }).click()
7676
})
7777
})

0 commit comments

Comments
 (0)