Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
4 changes: 4 additions & 0 deletions backend/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
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",
),
]
73 changes: 68 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,57 @@ 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(
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 ""
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)
suggested_location = open_ai.complete()
self.suggested_location = (
suggested_location if suggested_location and suggested_location != "None" else ""
)
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()
})
})
1 change: 1 addition & 0 deletions frontend/__tests__/unit/pages/Modal.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
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: 5 additions & 2 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,
entityType,
}: ModalProps) => {
return (
<DialogRoot
Expand Down Expand Up @@ -50,15 +51,17 @@ const Modal: React.FC<ModalProps> = ({
{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
The {entityType} summary and the recommended steps to address it have been generated by AI
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is unique entirely for different types of modal (issue, event). Pass the full text instead of entity

</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">
{entityType[0].toUpperCase() + entityType.substring(1)} 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}
entityType="issue"
></Modal>
</React.Fragment>
)
Expand Down
Loading