Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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)

# Suggested location.
if not event.suggested_location and (
prompt := Prompt.get_owasp_event_suggested_location()
):
event.generate_suggested_location(prompt)

# 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",
),
]
68 changes: 63 additions & 5 deletions backend/apps/owasp/models/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@
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.github.utils import normalize_url


Expand All @@ -30,13 +33,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 +138,53 @@ def from_dict(self, category, data):

for key, value in fields.items():
setattr(self, key, value)

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_context())
if location:
self.latitude = location.latitude
self.longitude = location.longitude

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 = (
suggested_location if suggested_location and suggested_location != "None" else ""
)
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,
)
8 changes: 4 additions & 4 deletions frontend/__tests__/e2e/pages/Home.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,17 +61,17 @@ test.describe('Home Page', () => {
test('should be able to join OWASP', async ({ page }) => {
await expect(page.getByRole('heading', { name: 'Ready to Make a Difference?' })).toBeVisible()
await expect(page.getByText('Join OWASP and be part of the')).toBeVisible()
await expect(page.getByRole('link', { name: 'Join OWASP Now' })).toBeVisible()
await expect(page.getByRole('link', { name: 'Join OWASP' })).toBeVisible()
const page1Promise = page.waitForEvent('popup')
await page.getByRole('link', { name: 'Join OWASP Now' }).click()
await page.getByRole('link', { name: 'Join OWASP' }).click()
const page1 = await page1Promise
expect(page1.url()).toBe('https://owasp.glueup.com/organization/6727/memberships/')
})

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
2 changes: 1 addition & 1 deletion frontend/src/components/ItemCardList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const ItemCardList = ({
}) => (
<SecondaryCard title={title}>
{data && data.length > 0 ? (
<div className="h-64 overflow-y-auto pr-2">
<div className="overflow-y-auto pr-2">
{data.map((item, index) => (
<div key={index} className="mb-4 w-full rounded-lg bg-gray-200 p-4 dark:bg-gray-700">
<div className="flex w-full flex-col justify-between">
Expand Down
10 changes: 6 additions & 4 deletions frontend/src/components/LogoCarousel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ export default function MovingLogos({ sponsors }: MovingLogosProps) {
</div>
<div className="mt-4 flex w-full flex-col items-center justify-center text-center text-sm text-muted-foreground">
<p>
These logos represent the corporate supporters of the OWASP Foundation, whose
contributions fuel OWASP's security initiatives. Visit{' '}
These logos represent the corporate supporters, whose contributions fuel OWASP Foundation
security initiatives. Visit{' '}
<a
href="https://owasp.org/supporters/"
className="font-medium text-primary hover:underline"
Expand All @@ -63,8 +63,10 @@ export default function MovingLogos({ sponsors }: MovingLogosProps) {
>
this page
</a>{' '}
to become an OWASP Foundation corporate supporter. If you're interested in sponsoring the
OWASP Nest project ❤️{' '}
to become a corporate supporter.
</p>
<p>
If you're interested in sponsoring the OWASP Nest project ❤️{' '}
<a
href="https://owasp.org/donate/?reponame=www-project-nest&title=OWASP+Nest"
className="font-medium text-primary hover:underline"
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
Loading