Skip to content
Open
6 changes: 5 additions & 1 deletion backend/make/apps/owasp.mk
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
.PHONY: owasp-add-project-custom-tags owasp-aggregate-entity-contributions \
owasp-aggregate-member-contributions owasp-aggregate-projects owasp-create-project-metadata-file \
owasp-aggregate-member-contributions owasp-aggregate-projects owasp-backfill-activity-events 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 \
Expand All @@ -23,6 +23,10 @@ owasp-aggregate-projects:
@echo "Aggregating OWASP projects"
@CMD="python manage.py owasp_aggregate_projects" $(MAKE) backend-exec-command

owasp-backfill-activity-events:
Comment thread
anurag2787 marked this conversation as resolved.
@echo "Backfilling activity events for existing pull requests, issues, and releases"
@CMD="python manage.py owasp_backfill_activity_events" $(MAKE) backend-exec-command

owasp-create-project-metadata-file:
@echo "Generating metadata"
@CMD="python manage.py owasp_create_project_metadata_file $(entity_key)" $(MAKE) backend-exec-command
Expand Down
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)

# 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:
ActivityEvent.update_data(release)
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",
)
Comment thread
anurag2787 marked this conversation as resolved.
list_filter = (
"activity_type",
"occurred_at",
)
search_fields = (
"activity_type",
"github_repository__name",
"github_user__login",
)


admin.site.register(ActivityEvent, ActivityEventAdmin)
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
"""A command to backfill activity events for existing pull requests, issues, and releases."""

import logging
from collections.abc import Callable
from typing import Any

from django.core.management.base import BaseCommand
from django.db.models import QuerySet

from apps.github.models.issue import Issue
from apps.github.models.pull_request import PullRequest
from apps.github.models.release import Release
from apps.owasp.models.activity_event import ActivityEvent

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


class Command(BaseCommand):
help = "Backfill ActivityEvent records for existing pull requests, issues, and releases."

def add_arguments(self, parser) -> None:
"""Add command-line arguments to the parser."""
parser.add_argument(
"--offset",
default=0,
required=False,
type=int,
Comment thread
anurag2787 marked this conversation as resolved.
help="Number of records to skip before starting backfill.",
)
parser.add_argument(
"--model",
default="all",
required=False,
choices=["all", "issue", "pull_request", "release"],
help="Which model type to backfill. Defaults to 'all'.",
)

def handle(self, *args, **options) -> None:
"""Handle the command execution."""
offset = options["offset"]
model = options["model"]

if model in ("all", "issue"):
self.backfill_issues(offset)

if model in ("all", "pull_request"):
self.backfill_pull_requests(offset)

if model in ("all", "release"):
self.backfill_releases(offset)

def backfill_objects(
self,
queryset: QuerySet,
offset: int,
noun: str,
get_label: Callable[[Any], str],
) -> None:
"""Backfill ActivityEvent records for a queryset of GitHub objects."""
count = queryset.count()
self.stdout.write(f"Backfilling activity events for {count} {noun}...\n")

created_count = 0
for obj in queryset[offset:].iterator(chunk_size=2000):
if not obj.repository:
logger.warning("Skipping %s %s: no repository", noun.rstrip("s"), get_label(obj))
continue

try:
ActivityEvent.update_data(obj)
created_count += 1
except Exception:
logger.exception(
"Error backfilling activity events for %s %s",
noun.rstrip("s"),
get_label(obj),
)

self.stdout.write(f"{noun.capitalize()} processed: {created_count}\n")

def backfill_issues(self, offset: int) -> None:
"""Backfill ActivityEvent records for existing issues."""
queryset = Issue.objects.select_related("author", "repository").order_by(
"created_at", "pk"
)
self.backfill_objects(queryset, offset, "issues", lambda obj: f"#{obj.number}")

def backfill_pull_requests(self, offset: int) -> None:
"""Backfill ActivityEvent records for existing pull requests."""
queryset = PullRequest.objects.select_related("author", "repository").order_by(
"created_at", "pk"
)
self.backfill_objects(queryset, offset, "pull requests", lambda obj: f"#{obj.number}")

def backfill_releases(self, offset: int) -> None:
"""Backfill ActivityEvent records for existing releases."""
queryset = Release.objects.select_related("author", "repository").order_by(
"created_at", "pk"
)
self.backfill_objects(queryset, offset, "releases", lambda obj: obj.tag_name)
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(
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