Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions backend/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ enrich-data: \
github-enrich-issues \
owasp-enrich-chapters \
owasp-enrich-committees \
owasp-enrich-events \
owasp-enrich-projects

generate-sitemap:
Expand Down Expand Up @@ -70,6 +71,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
Expand Down
10 changes: 10 additions & 0 deletions backend/apps/core/models/prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
5 changes: 4 additions & 1 deletion backend/apps/owasp/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,10 @@ class CommitteeAdmin(admin.ModelAdmin):


class EventAdmin(admin.ModelAdmin):
list_display = ("name",)
list_display = (
"name",
"suggested_location",
)
search_fields = ("name",)


Expand Down
2 changes: 2 additions & 0 deletions backend/apps/owasp/graphql/nodes/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,7 @@ class Meta:
"key",
"name",
"start_date",
"suggested_location",
"summary",
"url",
)
53 changes: 53 additions & 0 deletions backend/apps/owasp/management/commands/owasp_enrich_events.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""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")
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"),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# 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"),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# 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"),
),
]
Original file line number Diff line number Diff line change
@@ -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",
),
]
81 changes: 76 additions & 5 deletions backend/apps/owasp/models/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@
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.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


Expand All @@ -30,13 +34,18 @@ class Category(models.TextChoices):
choices=Category.choices,
default=Category.OTHER,
)

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")
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)
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."""
Expand Down Expand Up @@ -130,3 +139,65 @@ 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())
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Remove duplicate logic and fix the docstring
Lines 183–193 replicate the same OpenAI call found again in lines 194–203, but lack error handling. Consolidate them into a single block with proper exception handling, and correct the docstring to accurately describe the method purpose.

 def generate_suggested_location(self, open_ai=None, max_tokens=100):
-    """Generate project summary."""
-    if not (prompt := Prompt.get_owasp_event_suggested_location()):
-        return
+    """Generate a suggested location for the event using OpenAI."""
 
-    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 ""
-    )
+    prompt = Prompt.get_owasp_event_suggested_location()
+    if not prompt:
+        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)
     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 = ""
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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
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 a suggested location for the event using OpenAI."""
prompt = Prompt.get_owasp_event_suggested_location()
if not prompt:
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)
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 = ""

open_ai = open_ai or OpenAi()
open_ai.set_input(self.get_geo_string())
open_ai.set_max_tokens(max_tokens).set_prompt(prompt)
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 = ""
4 changes: 2 additions & 2 deletions frontend/__tests__/e2e/pages/Home.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ 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 — 28, 2025')).toBeVisible()
await page.getByRole('link', { name: 'Event 1' }).click()
await page.getByRole('button', { name: 'Event 1' }).click()
})
})
2 changes: 2 additions & 0 deletions frontend/__tests__/unit/pages/Modal.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ describe('Dialog Component', () => {
url: 'https://example.com/issue/123',
},
children: undefined as React.ReactNode | undefined,
description:
'The issue summary and the recommended steps to address it have been generated by AI',
}

const renderModal = (props = defaultProps) => {
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/api/queries/homeQueries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,11 @@ export const GET_MAIN_PAGE_DATA = gql`
upcomingEvents(limit: 6) {
category
endDate
key
name
startDate
summary
suggestedLocation
url
}
}
Expand Down
7 changes: 3 additions & 4 deletions frontend/src/components/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const Modal: React.FC<ModalProps> = ({
onClose,
button,
children,
description,
}: ModalProps) => {
return (
<DialogRoot
Expand All @@ -49,16 +50,14 @@ const Modal: React.FC<ModalProps> = ({
<DialogHeader className="mb-1 text-2xl font-bold text-gray-900 dark:text-white">
{title}
</DialogHeader>
<Text className="mb-1 text-xs text-gray-700 dark:text-gray-300/60">
The issue summary and the recommended steps to address it have been generated by AI
</Text>
<Text className="mb-1 text-xs text-gray-700 dark:text-gray-300/60">{description}</Text>
<Separator
variant="solid"
className="inset-0 -m-7 my-[.3rem] h-[.5px] border-gray-200 bg-gray-300 dark:bg-gray-700"
/>

<DialogBody>
<Text className="mb-2 text-xl font-semibold">Issue Summary</Text>
<Text className="mb-2 text-xl font-semibold">Summary</Text>
<Markdown className="text-base text-gray-600 dark:text-gray-300" content={summary} />
{hint && (
<Box className="rounded-md p-2">
Expand Down
1 change: 1 addition & 0 deletions frontend/src/pages/Contribute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ const ContributePage = () => {
summary={issue.summary}
hint={issue.hint}
button={viewIssueButton}
description="The issue summary and the recommended steps to address it have been generated by AI"
></Modal>
</React.Fragment>
)
Expand Down
Loading