Skip to content
4 changes: 4 additions & 0 deletions backend/make/apps/owasp.mk
Original file line number Diff line number Diff line change
Expand Up @@ -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,121 @@
"""A command to backfill activity events for existing pull requests, issues, and releases."""

import logging

from django.core.management.base import BaseCommand

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_issues(self, offset: int) -> None:
"""Backfill ActivityEvent records for existing issues."""
issues = Issue.objects.select_related("author", "repository").order_by("created_at")
Comment thread
anurag2787 marked this conversation as resolved.
Outdated
issues_count = issues.count()
self.stdout.write(f"Backfilling activity events for {issues_count} issues...\n")

created_count = 0
for issue in issues[offset:]:
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
Comment thread
anurag2787 marked this conversation as resolved.
Outdated

if not issue.repository:
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
logger.warning("Skipping issue #%s: no repository", issue.number)
continue

try:
ActivityEvent.update_data(issue)
created_count += 1
except Exception:
logger.exception("Error backfilling activity events for issue #%s", issue.number)

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

def backfill_pull_requests(self, offset: int) -> None:
"""Backfill ActivityEvent records for existing pull requests."""
pull_requests = PullRequest.objects.select_related("author", "repository").order_by(
"created_at"
)
pull_requests_count = pull_requests.count()
self.stdout.write(
f"Backfilling activity events for {pull_requests_count} pull requests...\n"
)

created_count = 0
for pull_request in pull_requests[offset:]:

if not pull_request.repository:
logger.warning(
"Skipping pull request #%s: no repository", pull_request.number
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
continue

try:
ActivityEvent.update_data(pull_request)
created_count += 1
except Exception:
logger.exception(
"Error backfilling activity events for pull request #%s",
pull_request.number,
)

self.stdout.write(f"Pull requests processed: {created_count}\n")

def backfill_releases(self, offset: int) -> None:
"""Backfill ActivityEvent records for existing releases."""
releases = Release.objects.select_related("author", "repository").order_by("created_at")
releases_count = releases.count()
self.stdout.write(f"Backfilling activity events for {releases_count} releases...\n")

created_count = 0
for release in releases[offset:]:

if not release.repository:
logger.warning("Skipping release %s: no repository", release.tag_name)
continue

try:
ActivityEvent.update_data(release)
created_count += 1
except Exception:
logger.exception(
"Error backfilling activity events for release %s", release.tag_name
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

self.stdout.write(f"Releases processed: {created_count}\n")
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
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