Skip to content
5 changes: 5 additions & 0 deletions backend/src/apps/github/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from apps.github.models.repository_contributor import RepositoryContributor
from apps.github.models.user import User
from apps.github.utils import check_owasp_site_repository
from apps.owasp.models.activity_event import ActivityEvent

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

Expand Down Expand Up @@ -138,6 +139,7 @@ def sync_repository(
milestone=milestone,
repository=repository,
)
ActivityEvent.update_data(issue)
Comment thread
anurag2787 marked this conversation as resolved.
Comment thread
anurag2787 marked this conversation as resolved.
Comment thread
anurag2787 marked this conversation as resolved.

# Assignees.
issue.assignees.clear()
Expand Down Expand Up @@ -186,6 +188,7 @@ def sync_repository(
milestone=milestone,
repository=repository,
)
ActivityEvent.update_data(pull_request)

# Assignees.
pull_request.assignees.clear()
Expand Down Expand Up @@ -217,6 +220,8 @@ def sync_repository(
author = User.update_data(gh_release.author)
releases.append(Release.update_data(gh_release, author=author, repository=repository))
Release.bulk_save(releases)
for release in releases:
Comment thread
anurag2787 marked this conversation as resolved.
ActivityEvent.update_data(release)
Comment thread
anurag2787 marked this conversation as resolved.
Comment thread
anurag2787 marked this conversation as resolved.

# GitHub repository contributors.
RepositoryContributor.bulk_save(
Expand Down
1 change: 1 addition & 0 deletions backend/src/apps/owasp/admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from apps.owasp.models.project_health_requirements import ProjectHealthRequirements

from .activity_event import ActivityEventAdmin
from .board_of_directors import BoardOfDirectorsAdmin
from .chapter import ChapterAdmin
from .committee import CommitteeAdmin
Expand Down
32 changes: 32 additions & 0 deletions backend/src/apps/owasp/admin/activity_event.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""OWASP app ActivityEvent model admin."""

from django.contrib import admin

from apps.owasp.models.activity_event import ActivityEvent


class ActivityEventAdmin(admin.ModelAdmin):
"""Admin for ActivityEvent model."""

autocomplete_fields = (
"github_user",
"github_repository",
)
list_display = (
"activity_type",
"github_repository",
"github_user",
"occurred_at",
)
list_filter = (
"activity_type",
"occurred_at",
)
search_fields = (
"activity_type",
"github_repository__name",
"github_user__login",
)


admin.site.register(ActivityEvent, ActivityEventAdmin)
53 changes: 53 additions & 0 deletions backend/src/apps/owasp/api/internal/nodes/activity_event.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""OWASP activity event GraphQL node."""

import strawberry
import strawberry_django

from apps.github.api.internal.nodes.repository import RepositoryNode
from apps.github.api.internal.nodes.user import UserNode
from apps.owasp.models.activity_event import ActivityEvent


@strawberry_django.type(
ActivityEvent,
fields=[
"activity_type",
"occurred_at",
],
)
class ActivityEventNode(strawberry.relay.Node):
"""Activity event node."""

@strawberry_django.field(select_related=["github_user"])
def github_user(self, root: ActivityEvent) -> UserNode | None:
"""Resolve GitHub user."""
return root.github_user

@strawberry_django.field(select_related=["github_repository"])
def github_repository(self, root: ActivityEvent) -> RepositoryNode:
"""Resolve GitHub repository."""
return root.github_repository

@strawberry_django.field
def number(self, root: ActivityEvent) -> int | None:
"""Resolve issue or PR number."""
return root.source_number

@strawberry_django.field
def title(self, root: ActivityEvent) -> str:
"""Resolve title of the source object."""
return root.source_title

@strawberry_django.field
def url(self, root: ActivityEvent) -> str:
"""Resolve URL of the source object."""
return root.source_url


@strawberry.type
class PaginatedActivityEvents:
"""A paginated list of activity events."""

current_page: int
events: list[ActivityEventNode]
total_pages: int
2 changes: 2 additions & 0 deletions backend/src/apps/owasp/api/internal/queries/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""OWASP GraphQL queries."""

from .activity_event import ActivityEventQuery
from .board_of_directors import BoardOfDirectorsQuery
from .chapter import ChapterQuery
from .committee import CommitteeQuery
Expand All @@ -14,6 +15,7 @@


class OwaspQuery(
ActivityEventQuery,
Comment thread
anurag2787 marked this conversation as resolved.
BoardOfDirectorsQuery,
ChapterQuery,
CommitteeQuery,
Expand Down
98 changes: 98 additions & 0 deletions backend/src/apps/owasp/api/internal/queries/activity_event.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"""OWASP activity event GraphQL queries."""

import strawberry
import strawberry_django

from apps.common.utils import normalize_limit
from apps.owasp.api.internal.nodes.activity_event import ActivityEventNode, PaginatedActivityEvents
from apps.owasp.models.activity_event import ActivityEvent
from apps.owasp.models.chapter import Chapter
from apps.owasp.models.project import Project

MAX_LIMIT = 1000
PAGE_SIZE = 20


@strawberry.type
class ActivityEventQuery:
"""Activity event queries."""

@strawberry_django.field
def activity_events(
self,
activity_type: str | None = None,
github_user_login: str | None = None,
project_key: str | None = None,
chapter_key: str | None = None,
time_range: str | None = None,
include_bots: bool = False,
order: str = "desc",
page: int = 1,
limit: int = PAGE_SIZE,
) -> PaginatedActivityEvents:
"""Resolve activity events with optional filtering and pagination."""
if (normalized_limit := normalize_limit(limit, MAX_LIMIT)) is None:
normalized_limit = PAGE_SIZE

if page < 1:
return PaginatedActivityEvents(current_page=1, events=[], total_pages=1)

if order not in {"asc", "desc"}:
return PaginatedActivityEvents(current_page=1, events=[], total_pages=1)

order_clause = "occurred_at" if order == "asc" else "-occurred_at"

queryset = ActivityEvent.objects.select_related(
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
"github_user",
"github_repository",
).order_by(order_clause)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated

if not include_bots:
queryset = ActivityEvent.exclude_bots(queryset)

if activity_type:
queryset = queryset.filter(activity_type=activity_type)

if github_user_login and (cleaned := github_user_login.strip()):
queryset = queryset.filter(github_user__login__icontains=cleaned)

if project_key and (cleaned := project_key.strip()):
project_repo_ids = Project.objects.filter(
key__icontains=cleaned,
).values_list("repositories", flat=True)
queryset = queryset.filter(github_repository__in=project_repo_ids)

if chapter_key and (cleaned := chapter_key.strip()):
chapter_repo_ids = Chapter.objects.filter(
key__icontains=cleaned,
).exclude(
owasp_repository__isnull=True,
).values_list("owasp_repository_id", flat=True)
queryset = queryset.filter(github_repository__in=chapter_repo_ids)

if time_range and (cleaned := time_range.strip()):
queryset = ActivityEvent.filter_time_range(queryset, cleaned)

total_count = queryset.count()
total_pages = max(1, (total_count + normalized_limit - 1) // normalized_limit)
page = max(1, min(page, total_pages))
offset = (page - 1) * normalized_limit

return PaginatedActivityEvents(
current_page=page,
events=list(queryset[offset : offset + normalized_limit]),
total_pages=total_pages,
)

@strawberry_django.field
def recent_activity_events(self, limit: int = 10) -> list[ActivityEventNode]:
"""Resolve recent activity events."""
if (normalized_limit := normalize_limit(limit, MAX_LIMIT)) is None:
return []

queryset = ActivityEvent.objects.select_related(
"github_user",
"github_repository",
).order_by("-occurred_at")

return list(ActivityEvent.exclude_bots(queryset)[:normalized_limit])
98 changes: 98 additions & 0 deletions backend/src/apps/owasp/migrations/0073_activityevent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Generated by Django 6.0.7 on 2026-07-28 07:54

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("contenttypes", "0002_remove_content_type_name"),
("github", "0044_user_indexes"),
("owasp", "0072_project_project_name_gin_idx_and_more"),
]

operations = [
migrations.CreateModel(
Comment thread
anurag2787 marked this conversation as resolved.
name="ActivityEvent",
fields=[
(
"id",
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
("nest_created_at", models.DateTimeField(auto_now_add=True)),
("nest_updated_at", models.DateTimeField(auto_now=True)),
(
"activity_type",
models.CharField(
choices=[
("issue_closed", "Issue Closed"),
("issue_opened", "Issue Opened"),
("pr_closed", "PR Closed"),
("pr_merged", "PR Merged"),
("pr_opened", "PR Opened"),
("release_published", "Release Published"),
],
max_length=32,
verbose_name="Activity Type",
),
),
("object_id", models.PositiveBigIntegerField()),
(
"occurred_at",
models.DateTimeField(
help_text="Timestamp when the activity event occurred on GitHub",
verbose_name="Occurred at",
),
),
(
"content_type",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="contenttypes.contenttype"
),
),
(
"github_repository",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="activity_events",
to="github.repository",
verbose_name="GitHub Repository",
),
),
(
"github_user",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="activity_events",
to="github.user",
verbose_name="GitHub User",
),
),
],
options={
"verbose_name_plural": "Activity Events",
"db_table": "github_activity_events",
"indexes": [
models.Index(fields=["activity_type"], name="activity_event_type_idx"),
models.Index(fields=["github_user"], name="activity_event_github_user_idx"),
models.Index(
fields=["content_type", "object_id"], name="activity_event_source_idx"
),
models.Index(fields=["occurred_at"], name="activity_event_occurred_at_idx"),
models.Index(
fields=["github_repository"], name="activity_event_github_repo_idx"
),
],
"constraints": [
models.UniqueConstraint(
fields=("activity_type", "content_type", "object_id", "occurred_at"),
name="unique_activity_event",
)
],
},
),
]
1 change: 1 addition & 0 deletions backend/src/apps/owasp/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from .activity_event import ActivityEvent
from .board_of_directors import BoardOfDirectors
from .chapter import Chapter
from .committee import Committee
Expand Down
Loading
Loading