Skip to content
Draft
Show file tree
Hide file tree
Changes from 12 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
10 changes: 7 additions & 3 deletions backend/make/apps/owasp.mk
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
owasp-aggregate-member-contributions owasp-aggregate-projects owasp-create-project-metadata-file \
owasp-enrich-chapters owasp-enrich-committees owasp-enrich-events owasp-enrich-projects \
owasp-generate-community-snapshot-video owasp-process-snapshots owasp-scrape-chapters \
owasp-scrape-committees owasp-scrape-projects owasp-sync-posts owasp-update-events \
owasp-update-leaders owasp-update-project-health-metrics owasp-update-project-health-requirements \
owasp-update-project-health-scores owasp-update-sponsors
owasp-scrape-committees owasp-scrape-projects owasp-sync-board-activity owasp-sync-posts \
owasp-update-events owasp-update-leaders owasp-update-project-health-metrics \
owasp-update-project-health-requirements owasp-update-project-health-scores owasp-update-sponsors

owasp-add-project-custom-tags:
@echo "Adding project custom tags from $(FILE)"
Expand Down Expand Up @@ -74,6 +74,10 @@ owasp-scrape-projects:
@echo "Scraping OWASP site projects data"
@CMD="python manage.py owasp_scrape_projects" $(MAKE) backend-exec-command

owasp-sync-board-activity:
@echo "Syncing OWASP board activity from www-board"
@CMD="python manage.py owasp_sync_board_activity $(ARGS)" $(MAKE) backend-exec-command

owasp-sync-posts:
@CMD="python manage.py owasp_sync_posts" $(MAKE) backend-exec-command

Expand Down
59 changes: 57 additions & 2 deletions backend/src/apps/common/open_ai.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,41 @@
from __future__ import annotations

import logging
from typing import TYPE_CHECKING, TypeVar

import openai
from django.conf import settings

if TYPE_CHECKING:
from pydantic import BaseModel

logger: logging.Logger = logging.getLogger(__name__)

T = TypeVar("T", bound="BaseModel")


class OpenAi:
"""Open AI communication class."""

def __init__(
self, model: str = "gpt-4o-mini", max_tokens: int = 1000, temperature: float = 0.7
self,
model: str = "gpt-4o-mini",
max_tokens: int = 1000,
temperature: float = 0.7,
timeout: int = 30,
) -> None:
"""OpenAi constructor.

Args:
model (str, optional): The model to use.
max_tokens (int, optional): Maximum tokens for the response.
temperature (float, optional): Sampling temperature.
timeout (int, optional): Request timeout in seconds. Defaults to 30.

"""
self.client = openai.OpenAI(
api_key=settings.OPEN_AI_SECRET_KEY,
timeout=30, # In seconds.
timeout=timeout,
)

self.max_tokens = max_tokens
Expand Down Expand Up @@ -75,6 +86,50 @@ def set_prompt(self, content: str) -> OpenAi:

return self

def parse(self, schema: type[T]) -> T | None:
"""Get a structured response validated against a pydantic schema.

Args:
schema (type[T]): A pydantic model class.

Returns:
T | None: A validated instance of the schema, or None on error.

"""
try:
Comment thread
rudransh-shrivastava marked this conversation as resolved.
response = self.client.beta.chat.completions.parse(
max_tokens=self.max_tokens,
messages=[
{"role": "system", "content": self.prompt},
{"role": "user", "content": self.input},
],
model=self.model,
response_format=schema,
temperature=self.temperature,
)
return response.choices[0].message.parsed
except openai.AuthenticationError:
logger.exception("OpenAI authentication failed: invalid or missing API key. ")
except openai.RateLimitError as e:
logger.warning(
"OpenAI rate limit exceeded: %s. Request may be retried with backoff.",
e,
)
except openai.BadRequestError:
logger.exception(
"OpenAI invalid request. Check model name, message format, and input size."
)
except openai.APIConnectionError:
logger.exception(
"OpenAI connection failed. Check network connectivity and firewall/proxy settings."
)
except Exception as e:
logger.exception(
"Unexpected OpenAI API error: %s",
type(e).__name__,
)
return None

def complete(self) -> str | None:
"""Get API response.

Expand Down
6 changes: 6 additions & 0 deletions backend/src/apps/owasp/admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@

from apps.owasp.models.project_health_requirements import ProjectHealthRequirements

from .board_discussion import BoardDiscussionAdmin
from .board_meeting import BoardMeetingAdmin
from .board_meeting_action import BoardMeetingActionAdmin
from .board_motion import BoardMotionAdmin
from .board_of_directors import BoardOfDirectorsAdmin
from .board_outcome import BoardOutcomeAdmin
from .board_vote import BoardVoteAdmin
from .chapter import ChapterAdmin
from .committee import CommitteeAdmin
from .entity_channel import EntityChannelAdmin
Expand Down
16 changes: 16 additions & 0 deletions backend/src/apps/owasp/admin/board_discussion.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""Board discussion admin configuration."""

from django.contrib import admin

from apps.owasp.models.board_discussion import BoardDiscussion


class BoardDiscussionAdmin(admin.ModelAdmin):
"""Admin for BoardDiscussion model."""

autocomplete_fields = ("participants",)
list_display = ("topic",)
search_fields = ("topic", "description")


admin.site.register(BoardDiscussion, BoardDiscussionAdmin)
18 changes: 18 additions & 0 deletions backend/src/apps/owasp/admin/board_meeting.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""Board meeting admin configuration."""

from django.contrib import admin

from apps.owasp.models.board_meeting import BoardMeeting


class BoardMeetingAdmin(admin.ModelAdmin):
"""Admin for BoardMeeting model."""

autocomplete_fields = ("board", "attendees", "absentees")
list_display = ("title", "date", "board", "type", "quorum_present")
list_filter = ("type", "quorum_present", "board__year")
ordering = ("-date",)
search_fields = ("title", "location", "source_path")


admin.site.register(BoardMeeting, BoardMeetingAdmin)
23 changes: 23 additions & 0 deletions backend/src/apps/owasp/admin/board_meeting_action.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""Board meeting action admin configuration."""

from django.contrib import admin

from apps.owasp.models.board_meeting_action import BoardMeetingAction


class BoardMeetingActionAdmin(admin.ModelAdmin):
"""Admin for BoardMeetingAction model."""

list_display = ("meeting", "order", "discussion", "motion", "outcome")
list_filter = ("meeting__type",)
ordering = ("meeting", "order")
raw_id_fields = ("meeting", "discussion", "motion", "outcome")
search_fields = (
"meeting__title",
"discussion__topic",
"motion__title",
"outcome__description",
)


admin.site.register(BoardMeetingAction, BoardMeetingActionAdmin)
16 changes: 16 additions & 0 deletions backend/src/apps/owasp/admin/board_motion.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""Board motion admin configuration."""

from django.contrib import admin

from apps.owasp.models.board_motion import BoardMotion


class BoardMotionAdmin(admin.ModelAdmin):
"""Admin for BoardMotion model."""

list_display = ("title", "sponsor", "second")
raw_id_fields = ("sponsor", "second", "amends_motion")
search_fields = ("title", "description", "background")


admin.site.register(BoardMotion, BoardMotionAdmin)
18 changes: 18 additions & 0 deletions backend/src/apps/owasp/admin/board_outcome.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""Board outcome admin configuration."""

from django.contrib import admin

from apps.owasp.models.board_outcome import BoardOutcome


class BoardOutcomeAdmin(admin.ModelAdmin):
"""Admin for BoardOutcome model."""

autocomplete_fields = ("assignees",)
list_display = ("description", "status", "due_date")
list_filter = ("status",)
ordering = ("-due_date",)
search_fields = ("description",)


admin.site.register(BoardOutcome, BoardOutcomeAdmin)
18 changes: 18 additions & 0 deletions backend/src/apps/owasp/admin/board_vote.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""Board vote admin configuration."""

from django.contrib import admin

from apps.owasp.models.board_vote import BoardVote


class BoardVoteAdmin(admin.ModelAdmin):
"""Admin for BoardVote model."""

autocomplete_fields = ("in_favor", "against", "abstain", "recused")
list_display = ("motion", "result", "type", "tally")
list_filter = ("result", "type")
raw_id_fields = ("motion",)
search_fields = ("motion__title", "tally")


admin.site.register(BoardVote, BoardVoteAdmin)
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""Sync board activity from OWASP/www-board into Django models."""

from django.core.management.base import BaseCommand, CommandError

from apps.owasp.parsers.board_activity import sync
from apps.owasp.parsers.board_activity.sync import MAX_YEAR, MIN_YEAR, SyncStatus

MIN_MONTH = 1
MAX_MONTH = 12


class Command(BaseCommand):
help = "Sync OWASP board meeting activity from the www-board repository."

def add_arguments(self, parser):
"""Add command-line arguments.

Args:
parser (argparse.ArgumentParser): The argument parser.

"""
parser.add_argument(
"--year",
type=int,
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
help="Only sync files whose filename begins with this 4-digit year.",
)
parser.add_argument(
"--month",
type=int,
help="Further restrict to a specific month (1-12). Requires --year.",
)
parser.add_argument(
"--path",
type=str,
help="Sync only a single repo-relative file path (ignores --year/--month).",
)
parser.add_argument(
"--force",
action="store_true",
help="Re-parse even when the stored git blob SHA matches.",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Parse and log intended writes without persisting.",
)

def handle(self, *args, **options):
"""Run the board activity sync.

Raises:
CommandError: If --month or --year is invalid, or if any file failed to sync.

"""
year = options.get("year")
month = options.get("month")
path = options.get("path")

if path is None:
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
Comment thread
rudransh-shrivastava marked this conversation as resolved.
Outdated
if month is not None and year is None:
message = "--month requires --year."
raise CommandError(message)

if month is not None and not (MIN_MONTH <= month <= MAX_MONTH):
message = f"--month must be between {MIN_MONTH} and {MAX_MONTH}."
raise CommandError(message)

if year is not None and not (MIN_YEAR <= year <= MAX_YEAR):
message = f"--year must be a 4-digit value between {MIN_YEAR} and {MAX_YEAR}."
raise CommandError(message)

stats = sync.run(
year=year,
month=month,
path=path,
force=options.get("force", False),
dry_run=options.get("dry_run", False),
)

summary = ", ".join(f"{k}={v}" for k, v in sorted(stats.counts.items())) or "no files"
self.stdout.write(self.style.SUCCESS(f"Board activity sync: {summary}"))
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.

errored = stats.counts.get(SyncStatus.ERRORED, 0)
if errored:
message = f"Board activity sync had {errored} errored file(s)."
raise CommandError(message)
Loading
Loading