From ecd8bb6a07b7045b60afe3e8ee05511b4f289ba6 Mon Sep 17 00:00:00 2001 From: Anurag Yadav Date: Sat, 18 Jul 2026 22:08:21 +0530 Subject: [PATCH 1/9] Implemented data model Signed-off-by: Anurag Yadav --- backend/src/apps/github/admin/__init__.py | 1 + .../src/apps/github/admin/activity_event.py | 33 ++++++++ .../github/migrations/0045_activityevent.py | 35 +++++++++ backend/src/apps/github/models/__init__.py | 1 + .../src/apps/github/models/activity_event.py | 78 +++++++++++++++++++ .../src/apps/github/models/enums/__init__.py | 0 .../github/models/enums/activity_event.py | 18 +++++ docker-compose/local/compose.yaml | 26 +++---- 8 files changed, 179 insertions(+), 13 deletions(-) create mode 100644 backend/src/apps/github/admin/activity_event.py create mode 100644 backend/src/apps/github/migrations/0045_activityevent.py create mode 100644 backend/src/apps/github/models/activity_event.py create mode 100644 backend/src/apps/github/models/enums/__init__.py create mode 100644 backend/src/apps/github/models/enums/activity_event.py diff --git a/backend/src/apps/github/admin/__init__.py b/backend/src/apps/github/admin/__init__.py index 60ae27406f..eb7170e91f 100644 --- a/backend/src/apps/github/admin/__init__.py +++ b/backend/src/apps/github/admin/__init__.py @@ -1,5 +1,6 @@ """Github app admin.""" +from .activity_event import ActivityEventAdmin from .comment import CommentAdmin from .commit import CommitAdmin from .issue import IssueAdmin diff --git a/backend/src/apps/github/admin/activity_event.py b/backend/src/apps/github/admin/activity_event.py new file mode 100644 index 0000000000..715aba9cd5 --- /dev/null +++ b/backend/src/apps/github/admin/activity_event.py @@ -0,0 +1,33 @@ +"""GitHub app ActivityEvent model admin.""" + +from django.contrib import admin + +from apps.github.models.activity_event import ActivityEvent + + +class ActivityEventAdmin(admin.ModelAdmin): + """Admin for ActivityEvent model.""" + + autocomplete_fields = ( + "actor", + "repository", + ) + list_display = ( + "activity_type", + "actor", + "nest_created_at", + "occurred_at", + "repository", + ) + list_filter = ( + "activity_type", + "occurred_at", + ) + search_fields = ( + "activity_type", + "actor__login", + "repository__name", + ) + + +admin.site.register(ActivityEvent, ActivityEventAdmin) diff --git a/backend/src/apps/github/migrations/0045_activityevent.py b/backend/src/apps/github/migrations/0045_activityevent.py new file mode 100644 index 0000000000..046aa38f51 --- /dev/null +++ b/backend/src/apps/github/migrations/0045_activityevent.py @@ -0,0 +1,35 @@ +# Generated by Django 6.0.7 on 2026-07-17 08:13 + +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'), + ] + + 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=[('pr_opened', 'PR Opened'), ('pr_closed', 'PR Closed'), ('pr_merged', 'PR Merged'), ('issue_opened', 'Issue Opened'), ('issue_closed', 'Issue Closed'), ('commit_pushed', 'Commit Pushed'), ('release_published', 'Release Published')], max_length=32, verbose_name='Activity Type')), + ('occurred_at', models.DateTimeField(help_text='Timestamp when the activity event occurred on GitHub', verbose_name='Occurred at')), + ('object_id', models.PositiveBigIntegerField()), + ('actor', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='activity_events', to='github.user', verbose_name='Actor')), + ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')), + ('repository', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='activity_events', to='github.repository', verbose_name='Repository')), + ], + options={ + 'verbose_name_plural': 'Activity Events', + 'db_table': 'github_activity_events', + 'indexes': [models.Index(fields=['occurred_at'], name='activity_event_occurred_at_idx'), models.Index(fields=['activity_type'], name='activity_event_type_idx'), models.Index(fields=['repository'], name='activity_event_repository_idx'), models.Index(fields=['actor'], name='activity_event_actor_idx'), models.Index(fields=['content_type', 'object_id'], name='activity_event_source_idx')], + 'constraints': [models.UniqueConstraint(fields=('activity_type', 'content_type', 'object_id'), name='unique_activity_event')], + }, + ), + ] diff --git a/backend/src/apps/github/models/__init__.py b/backend/src/apps/github/models/__init__.py index 5b4113b60b..d25fed562a 100644 --- a/backend/src/apps/github/models/__init__.py +++ b/backend/src/apps/github/models/__init__.py @@ -1,5 +1,6 @@ """Github app.""" +from .activity_event import ActivityEvent from .comment import Comment from .commit import Commit from .issue import Issue diff --git a/backend/src/apps/github/models/activity_event.py b/backend/src/apps/github/models/activity_event.py new file mode 100644 index 0000000000..19bcc14f03 --- /dev/null +++ b/backend/src/apps/github/models/activity_event.py @@ -0,0 +1,78 @@ +"""Github app activity event model.""" + +from django.contrib.contenttypes.fields import GenericForeignKey +from django.contrib.contenttypes.models import ContentType +from django.db import models + +from apps.common.models import BulkSaveModel, TimestampedModel + + +from apps.github.models.enums.activity_event import ActivityType + + +class ActivityEvent(BulkSaveModel, TimestampedModel): + """Represents a discrete GitHub activity event linked to a single source object via a polymorphic GenericForeignKey.""" + + class Meta: + """Model options.""" + + db_table = "github_activity_events" + verbose_name_plural = "Activity Events" + + constraints = [ + models.UniqueConstraint( + fields=[ + "activity_type", + "content_type", + "object_id", + ], + name="unique_activity_event", + ), + ] + + indexes = [ + models.Index(fields=["activity_type"], name="activity_event_type_idx"), + models.Index(fields=["actor"], name="activity_event_actor_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=["repository"], name="activity_event_repository_idx"), + ] + + activity_type = models.CharField( + verbose_name="Activity Type", + max_length=32, + choices=ActivityType.choices, + ) + actor = models.ForeignKey( + "github.User", + verbose_name="Actor", + on_delete=models.SET_NULL, + blank=True, + null=True, + related_name="activity_events", + ) + content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) + object_id = models.PositiveBigIntegerField() + occurred_at = models.DateTimeField( + verbose_name="Occurred at", + help_text="Timestamp when the activity event occurred on GitHub", + ) + repository = models.ForeignKey( + "github.Repository", + verbose_name="Repository", + on_delete=models.CASCADE, + related_name="activity_events", + ) + source_object = GenericForeignKey("content_type", "object_id") + + def __str__(self) -> str: + """Return human-readable representation.""" + return f"{self.activity_type} by {self.actor} in {self.repository}" + + @staticmethod + def bulk_save(activity_events, fields=None) -> None: # type: ignore[override] + """Bulk save activity events.""" + BulkSaveModel.bulk_save(ActivityEvent, activity_events, fields=fields) diff --git a/backend/src/apps/github/models/enums/__init__.py b/backend/src/apps/github/models/enums/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/backend/src/apps/github/models/enums/activity_event.py b/backend/src/apps/github/models/enums/activity_event.py new file mode 100644 index 0000000000..caffcd055a --- /dev/null +++ b/backend/src/apps/github/models/enums/activity_event.py @@ -0,0 +1,18 @@ +"""Enums for GitHub activity events.""" + +from django.db import models + + +class ActivityType(models.TextChoices): + """Activity type choices.""" + + COMMIT_PUSHED = "commit_pushed", "Commit Pushed" + + ISSUE_CLOSED = "issue_closed", "Issue Closed" + ISSUE_OPENED = "issue_opened", "Issue Opened" + + PR_CLOSED = "pr_closed", "PR Closed" + PR_MERGED = "pr_merged", "PR Merged" + PR_OPENED = "pr_opened", "PR Opened" + + RELEASE_PUBLISHED = "release_published", "Release Published" diff --git a/docker-compose/local/compose.yaml b/docker-compose/local/compose.yaml index 8b04f7b329..f9a8a056d0 100644 --- a/docker-compose/local/compose.yaml +++ b/docker-compose/local/compose.yaml @@ -23,7 +23,7 @@ services: - 8000:8000 volumes: - ../../backend:/home/owasp - - backend-venv:/home/owasp/.venv + - backend-venv-pulse:/home/owasp/.venv cache: command: > @@ -41,7 +41,7 @@ services: networks: - nest-network volumes: - - cache-data:/data + - cache-data-pulse:/data db: container_name: nest-db @@ -55,7 +55,7 @@ services: networks: - nest-network volumes: - - db-data-5079:/var/lib/postgresql/data + - db-data-pulse:/var/lib/postgresql/data docs: container_name: nest-docs @@ -77,7 +77,7 @@ services: - ../../README.md:/home/owasp/README.md:ro - ../../CODE_OF_CONDUCT.md:/home/owasp/CODE_OF_CONDUCT.md:ro - ../../CONTRIBUTING.md:/home/owasp/CONTRIBUTING.md:ro - - docs-venv:/home/owasp/.venv + - docs-venv-pulse:/home/owasp/.venv frontend: container_name: nest-frontend @@ -99,8 +99,8 @@ services: - 3000:3000 volumes: - ../../frontend:/home/owasp - - frontend-next:/home/owasp/.next - - frontend-node-modules:/home/owasp/node_modules + - frontend-next-pulse:/home/owasp/.next + - frontend-node-modules-pulse:/home/owasp/node_modules worker: container_name: nest-worker @@ -121,15 +121,15 @@ services: - nest-network volumes: - ../../backend:/home/owasp - - backend-venv:/home/owasp/.venv + - backend-venv-pulse:/home/owasp/.venv networks: nest-network: volumes: - backend-venv: - cache-data: - db-data-5079: - docs-venv: - frontend-next: - frontend-node-modules: + backend-venv-pulse: + cache-data-pulse: + db-data-pulse: + docs-venv-pulse: + frontend-next-pulse: + frontend-node-modules-pulse: From 6c15d09b4d9b1cd5d92de2bd734f9b02b0243927 Mon Sep 17 00:00:00 2001 From: Anurag Yadav Date: Sat, 18 Jul 2026 22:54:27 +0530 Subject: [PATCH 2/9] adress review Signed-off-by: Anurag Yadav --- .../github/migrations/0045_activityevent.py | 97 +++++++++++++++---- .../src/apps/github/models/activity_event.py | 8 +- .../github/models/enums/activity_event.py | 3 - 3 files changed, 84 insertions(+), 24 deletions(-) diff --git a/backend/src/apps/github/migrations/0045_activityevent.py b/backend/src/apps/github/migrations/0045_activityevent.py index 046aa38f51..cba54f7b0c 100644 --- a/backend/src/apps/github/migrations/0045_activityevent.py +++ b/backend/src/apps/github/migrations/0045_activityevent.py @@ -1,35 +1,96 @@ -# Generated by Django 6.0.7 on 2026-07-17 08:13 +# Generated by Django 6.0.7 on 2026-07-18 17:15 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'), + ("contenttypes", "0002_remove_content_type_name"), + ("github", "0044_user_indexes"), ] operations = [ migrations.CreateModel( - name='ActivityEvent', + 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=[('pr_opened', 'PR Opened'), ('pr_closed', 'PR Closed'), ('pr_merged', 'PR Merged'), ('issue_opened', 'Issue Opened'), ('issue_closed', 'Issue Closed'), ('commit_pushed', 'Commit Pushed'), ('release_published', 'Release Published')], max_length=32, verbose_name='Activity Type')), - ('occurred_at', models.DateTimeField(help_text='Timestamp when the activity event occurred on GitHub', verbose_name='Occurred at')), - ('object_id', models.PositiveBigIntegerField()), - ('actor', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='activity_events', to='github.user', verbose_name='Actor')), - ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')), - ('repository', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='activity_events', to='github.repository', verbose_name='Repository')), + ( + "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=[ + ("commit_pushed", "Commit Pushed"), + ("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", + ), + ), + ( + "actor", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="activity_events", + to="github.user", + verbose_name="Actor", + ), + ), + ( + "content_type", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="contenttypes.contenttype" + ), + ), + ( + "repository", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="activity_events", + to="github.repository", + verbose_name="Repository", + ), + ), ], options={ - 'verbose_name_plural': 'Activity Events', - 'db_table': 'github_activity_events', - 'indexes': [models.Index(fields=['occurred_at'], name='activity_event_occurred_at_idx'), models.Index(fields=['activity_type'], name='activity_event_type_idx'), models.Index(fields=['repository'], name='activity_event_repository_idx'), models.Index(fields=['actor'], name='activity_event_actor_idx'), models.Index(fields=['content_type', 'object_id'], name='activity_event_source_idx')], - 'constraints': [models.UniqueConstraint(fields=('activity_type', 'content_type', 'object_id'), name='unique_activity_event')], + "verbose_name_plural": "Activity Events", + "db_table": "github_activity_events", + "indexes": [ + models.Index(fields=["activity_type"], name="activity_event_type_idx"), + models.Index(fields=["actor"], name="activity_event_actor_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=["repository"], name="activity_event_repository_idx"), + ], + "constraints": [ + models.UniqueConstraint( + fields=("activity_type", "content_type", "object_id", "occurred_at"), + name="unique_activity_event", + ) + ], }, ), ] diff --git a/backend/src/apps/github/models/activity_event.py b/backend/src/apps/github/models/activity_event.py index 19bcc14f03..586eacac1b 100644 --- a/backend/src/apps/github/models/activity_event.py +++ b/backend/src/apps/github/models/activity_event.py @@ -5,13 +5,14 @@ from django.db import models from apps.common.models import BulkSaveModel, TimestampedModel - - from apps.github.models.enums.activity_event import ActivityType class ActivityEvent(BulkSaveModel, TimestampedModel): - """Represents a discrete GitHub activity event linked to a single source object via a polymorphic GenericForeignKey.""" + """Represents a discrete GitHub activity event linked to a single source object. + + Uses a polymorphic GenericForeignKey to reference the source object. + """ class Meta: """Model options.""" @@ -25,6 +26,7 @@ class Meta: "activity_type", "content_type", "object_id", + "occurred_at", ], name="unique_activity_event", ), diff --git a/backend/src/apps/github/models/enums/activity_event.py b/backend/src/apps/github/models/enums/activity_event.py index caffcd055a..ea998da463 100644 --- a/backend/src/apps/github/models/enums/activity_event.py +++ b/backend/src/apps/github/models/enums/activity_event.py @@ -7,12 +7,9 @@ class ActivityType(models.TextChoices): """Activity type choices.""" COMMIT_PUSHED = "commit_pushed", "Commit Pushed" - ISSUE_CLOSED = "issue_closed", "Issue Closed" ISSUE_OPENED = "issue_opened", "Issue Opened" - PR_CLOSED = "pr_closed", "PR Closed" PR_MERGED = "pr_merged", "PR Merged" PR_OPENED = "pr_opened", "PR Opened" - RELEASE_PUBLISHED = "release_published", "Release Published" From 154b03c823cfbfa422f59485961c2857518962c3 Mon Sep 17 00:00:00 2001 From: Anurag Yadav Date: Wed, 29 Jul 2026 15:41:40 +0530 Subject: [PATCH 3/9] added actitivty builder Signed-off-by: Anurag Yadav --- backend/src/apps/github/admin/__init__.py | 1 - backend/src/apps/github/common.py | 5 + .../github/migrations/0045_activityevent.py | 96 ----------- backend/src/apps/github/models/__init__.py | 1 - .../src/apps/github/models/activity_event.py | 80 --------- .../src/apps/github/models/enums/__init__.py | 0 .../github/models/enums/activity_event.py | 15 -- backend/src/apps/owasp/admin/__init__.py | 1 + .../{github => owasp}/admin/activity_event.py | 17 +- .../owasp/migrations/0073_activityevent.py | 36 +++++ backend/src/apps/owasp/models/__init__.py | 1 + .../src/apps/owasp/models/activity_event.py | 153 ++++++++++++++++++ backend/tests/unit/apps/github/common_test.py | 1 + 13 files changed, 205 insertions(+), 202 deletions(-) delete mode 100644 backend/src/apps/github/migrations/0045_activityevent.py delete mode 100644 backend/src/apps/github/models/activity_event.py delete mode 100644 backend/src/apps/github/models/enums/__init__.py delete mode 100644 backend/src/apps/github/models/enums/activity_event.py rename backend/src/apps/{github => owasp}/admin/activity_event.py (60%) create mode 100644 backend/src/apps/owasp/migrations/0073_activityevent.py create mode 100644 backend/src/apps/owasp/models/activity_event.py diff --git a/backend/src/apps/github/admin/__init__.py b/backend/src/apps/github/admin/__init__.py index eb7170e91f..60ae27406f 100644 --- a/backend/src/apps/github/admin/__init__.py +++ b/backend/src/apps/github/admin/__init__.py @@ -1,6 +1,5 @@ """Github app admin.""" -from .activity_event import ActivityEventAdmin from .comment import CommentAdmin from .commit import CommitAdmin from .issue import IssueAdmin diff --git a/backend/src/apps/github/common.py b/backend/src/apps/github/common.py index 1371fcf782..eb0abfe3d7 100644 --- a/backend/src/apps/github/common.py +++ b/backend/src/apps/github/common.py @@ -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__) @@ -138,6 +139,7 @@ def sync_repository( milestone=milestone, repository=repository, ) + ActivityEvent.update_data(issue) # Assignees. issue.assignees.clear() @@ -186,6 +188,7 @@ def sync_repository( milestone=milestone, repository=repository, ) + ActivityEvent.update_data(pull_request) # Assignees. pull_request.assignees.clear() @@ -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) # GitHub repository contributors. RepositoryContributor.bulk_save( diff --git a/backend/src/apps/github/migrations/0045_activityevent.py b/backend/src/apps/github/migrations/0045_activityevent.py deleted file mode 100644 index cba54f7b0c..0000000000 --- a/backend/src/apps/github/migrations/0045_activityevent.py +++ /dev/null @@ -1,96 +0,0 @@ -# Generated by Django 6.0.7 on 2026-07-18 17:15 - -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"), - ] - - 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=[ - ("commit_pushed", "Commit Pushed"), - ("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", - ), - ), - ( - "actor", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="activity_events", - to="github.user", - verbose_name="Actor", - ), - ), - ( - "content_type", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to="contenttypes.contenttype" - ), - ), - ( - "repository", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="activity_events", - to="github.repository", - verbose_name="Repository", - ), - ), - ], - 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=["actor"], name="activity_event_actor_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=["repository"], name="activity_event_repository_idx"), - ], - "constraints": [ - models.UniqueConstraint( - fields=("activity_type", "content_type", "object_id", "occurred_at"), - name="unique_activity_event", - ) - ], - }, - ), - ] diff --git a/backend/src/apps/github/models/__init__.py b/backend/src/apps/github/models/__init__.py index d25fed562a..5b4113b60b 100644 --- a/backend/src/apps/github/models/__init__.py +++ b/backend/src/apps/github/models/__init__.py @@ -1,6 +1,5 @@ """Github app.""" -from .activity_event import ActivityEvent from .comment import Comment from .commit import Commit from .issue import Issue diff --git a/backend/src/apps/github/models/activity_event.py b/backend/src/apps/github/models/activity_event.py deleted file mode 100644 index 586eacac1b..0000000000 --- a/backend/src/apps/github/models/activity_event.py +++ /dev/null @@ -1,80 +0,0 @@ -"""Github app activity event model.""" - -from django.contrib.contenttypes.fields import GenericForeignKey -from django.contrib.contenttypes.models import ContentType -from django.db import models - -from apps.common.models import BulkSaveModel, TimestampedModel -from apps.github.models.enums.activity_event import ActivityType - - -class ActivityEvent(BulkSaveModel, TimestampedModel): - """Represents a discrete GitHub activity event linked to a single source object. - - Uses a polymorphic GenericForeignKey to reference the source object. - """ - - class Meta: - """Model options.""" - - db_table = "github_activity_events" - verbose_name_plural = "Activity Events" - - constraints = [ - models.UniqueConstraint( - fields=[ - "activity_type", - "content_type", - "object_id", - "occurred_at", - ], - name="unique_activity_event", - ), - ] - - indexes = [ - models.Index(fields=["activity_type"], name="activity_event_type_idx"), - models.Index(fields=["actor"], name="activity_event_actor_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=["repository"], name="activity_event_repository_idx"), - ] - - activity_type = models.CharField( - verbose_name="Activity Type", - max_length=32, - choices=ActivityType.choices, - ) - actor = models.ForeignKey( - "github.User", - verbose_name="Actor", - on_delete=models.SET_NULL, - blank=True, - null=True, - related_name="activity_events", - ) - content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) - object_id = models.PositiveBigIntegerField() - occurred_at = models.DateTimeField( - verbose_name="Occurred at", - help_text="Timestamp when the activity event occurred on GitHub", - ) - repository = models.ForeignKey( - "github.Repository", - verbose_name="Repository", - on_delete=models.CASCADE, - related_name="activity_events", - ) - source_object = GenericForeignKey("content_type", "object_id") - - def __str__(self) -> str: - """Return human-readable representation.""" - return f"{self.activity_type} by {self.actor} in {self.repository}" - - @staticmethod - def bulk_save(activity_events, fields=None) -> None: # type: ignore[override] - """Bulk save activity events.""" - BulkSaveModel.bulk_save(ActivityEvent, activity_events, fields=fields) diff --git a/backend/src/apps/github/models/enums/__init__.py b/backend/src/apps/github/models/enums/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/backend/src/apps/github/models/enums/activity_event.py b/backend/src/apps/github/models/enums/activity_event.py deleted file mode 100644 index ea998da463..0000000000 --- a/backend/src/apps/github/models/enums/activity_event.py +++ /dev/null @@ -1,15 +0,0 @@ -"""Enums for GitHub activity events.""" - -from django.db import models - - -class ActivityType(models.TextChoices): - """Activity type choices.""" - - COMMIT_PUSHED = "commit_pushed", "Commit Pushed" - ISSUE_CLOSED = "issue_closed", "Issue Closed" - ISSUE_OPENED = "issue_opened", "Issue Opened" - PR_CLOSED = "pr_closed", "PR Closed" - PR_MERGED = "pr_merged", "PR Merged" - PR_OPENED = "pr_opened", "PR Opened" - RELEASE_PUBLISHED = "release_published", "Release Published" diff --git a/backend/src/apps/owasp/admin/__init__.py b/backend/src/apps/owasp/admin/__init__.py index 261225145a..524c70a757 100644 --- a/backend/src/apps/owasp/admin/__init__.py +++ b/backend/src/apps/owasp/admin/__init__.py @@ -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 diff --git a/backend/src/apps/github/admin/activity_event.py b/backend/src/apps/owasp/admin/activity_event.py similarity index 60% rename from backend/src/apps/github/admin/activity_event.py rename to backend/src/apps/owasp/admin/activity_event.py index 715aba9cd5..72da546259 100644 --- a/backend/src/apps/github/admin/activity_event.py +++ b/backend/src/apps/owasp/admin/activity_event.py @@ -1,23 +1,22 @@ -"""GitHub app ActivityEvent model admin.""" +"""OWASP app ActivityEvent model admin.""" from django.contrib import admin -from apps.github.models.activity_event import ActivityEvent +from apps.owasp.models.activity_event import ActivityEvent class ActivityEventAdmin(admin.ModelAdmin): """Admin for ActivityEvent model.""" autocomplete_fields = ( - "actor", - "repository", + "github_user", + "github_repository", ) list_display = ( "activity_type", - "actor", - "nest_created_at", + "github_user", "occurred_at", - "repository", + "github_repository", ) list_filter = ( "activity_type", @@ -25,8 +24,8 @@ class ActivityEventAdmin(admin.ModelAdmin): ) search_fields = ( "activity_type", - "actor__login", - "repository__name", + "github_user__login", + "github_repository__name", ) diff --git a/backend/src/apps/owasp/migrations/0073_activityevent.py b/backend/src/apps/owasp/migrations/0073_activityevent.py new file mode 100644 index 0000000000..f5cf1fbc03 --- /dev/null +++ b/backend/src/apps/owasp/migrations/0073_activityevent.py @@ -0,0 +1,36 @@ +# 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')], + }, + ), + ] diff --git a/backend/src/apps/owasp/models/__init__.py b/backend/src/apps/owasp/models/__init__.py index 3cbb120b8b..3bbb4a7832 100644 --- a/backend/src/apps/owasp/models/__init__.py +++ b/backend/src/apps/owasp/models/__init__.py @@ -1,3 +1,4 @@ +from .activity_event import ActivityEvent from .board_of_directors import BoardOfDirectors from .chapter import Chapter from .committee import Committee diff --git a/backend/src/apps/owasp/models/activity_event.py b/backend/src/apps/owasp/models/activity_event.py new file mode 100644 index 0000000000..79b2194ffd --- /dev/null +++ b/backend/src/apps/owasp/models/activity_event.py @@ -0,0 +1,153 @@ +"""OWASP app activity event model.""" + +import logging + +from django.contrib.contenttypes.fields import GenericForeignKey +from django.contrib.contenttypes.models import ContentType +from django.db import models + +from apps.common.models import BulkSaveModel, TimestampedModel + +logger = logging.getLogger(__name__) + + +class ActivityEvent(BulkSaveModel, TimestampedModel): + """Represents a discrete GitHub activity event linked to a single source object. + + Uses a polymorphic GenericForeignKey to reference the source object. + """ + + class Meta: + """Model options.""" + + db_table = "github_activity_events" + verbose_name_plural = "Activity Events" + + constraints = [ + models.UniqueConstraint( + fields=[ + "activity_type", + "content_type", + "object_id", + "occurred_at", + ], + name="unique_activity_event", + ), + ] + + 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"), + ] + + class ActivityType(models.TextChoices): + """Activity type choices.""" + + ISSUE_CLOSED = "issue_closed", "Issue Closed" + ISSUE_OPENED = "issue_opened", "Issue Opened" + PR_CLOSED = "pr_closed", "PR Closed" + PR_MERGED = "pr_merged", "PR Merged" + PR_OPENED = "pr_opened", "PR Opened" + RELEASE_PUBLISHED = "release_published", "Release Published" + + activity_type = models.CharField( + verbose_name="Activity Type", + max_length=32, + choices=ActivityType.choices, + ) + github_user = models.ForeignKey( + "github.User", + verbose_name="GitHub User", + on_delete=models.SET_NULL, + blank=True, + null=True, + related_name="activity_events", + ) + content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) + object_id = models.PositiveBigIntegerField() + occurred_at = models.DateTimeField( + verbose_name="Occurred at", + help_text="Timestamp when the activity event occurred on GitHub", + ) + github_repository = models.ForeignKey( + "github.Repository", + verbose_name="GitHub Repository", + on_delete=models.CASCADE, + related_name="activity_events", + ) + source_object = GenericForeignKey("content_type", "object_id") + + HANDLERS: dict[str, str] = { + "Issue": "build_for_issue", + "PullRequest": "build_for_pull_request", + "Release": "build_for_release", + } + + def __str__(self) -> str: + """Return human-readable representation.""" + return f"{self.activity_type} by {self.github_user} in {self.github_repository}" + + @staticmethod + def bulk_save(activity_events, fields=None) -> None: # type: ignore[override] + """Bulk save activity events.""" + BulkSaveModel.bulk_save(ActivityEvent, activity_events, fields=fields) + + @staticmethod + def build_for_issue(issue) -> list[tuple]: + """Return event tuples for an Issue.""" + events = [(ActivityEvent.ActivityType.ISSUE_OPENED, issue.created_at, issue.author)] + if issue.state == "closed" and issue.closed_at: + events.append((ActivityEvent.ActivityType.ISSUE_CLOSED, issue.closed_at, issue.author)) + return events + + @staticmethod + def build_for_pull_request(pr) -> list[tuple]: + """Return event tuples for a PullRequest.""" + events = [(ActivityEvent.ActivityType.PR_OPENED, pr.created_at, pr.author)] + if pr.merged_at: + events.append((ActivityEvent.ActivityType.PR_MERGED, pr.merged_at, pr.author)) + elif pr.state == "closed" and pr.closed_at: + events.append((ActivityEvent.ActivityType.PR_CLOSED, pr.closed_at, pr.author)) + return events + + @staticmethod + def build_for_release(release) -> list[tuple]: + """Return event tuples for a Release.""" + occurred_at = release.published_at or release.created_at + return [(ActivityEvent.ActivityType.RELEASE_PUBLISHED, occurred_at, release.author)] + + @staticmethod + def update_data(obj) -> None: + """Create ActivityEvent row(s) for a saved GitHub model instance if they do not exist.""" + handler_name = ActivityEvent.HANDLERS.get(type(obj).__name__) + if handler_name is None: + logger.error( + "ActivityEvent.update_data received unsupported model type: %s", + type(obj).__name__, + ) + raise TypeError(f"Unsupported model type: {type(obj)}") + + handler = getattr(ActivityEvent, handler_name) + events = handler(obj) + content_type = ContentType.objects.get_for_model(obj) + + for activity_type, occurred_at, github_user in events: + if occurred_at is None: + continue + + ActivityEvent.objects.get_or_create( + activity_type=activity_type, + content_type=content_type, + object_id=obj.pk, + occurred_at=occurred_at, + defaults={ + "github_user": github_user, + "github_repository": obj.repository, + }, + ) diff --git a/backend/tests/unit/apps/github/common_test.py b/backend/tests/unit/apps/github/common_test.py index a5501eed1c..8106f85584 100644 --- a/backend/tests/unit/apps/github/common_test.py +++ b/backend/tests/unit/apps/github/common_test.py @@ -21,6 +21,7 @@ def mock_common_deps(mocker): "Label": mocker.patch("apps.github.common.Label"), "Release": mocker.patch("apps.github.common.Release"), "RepositoryContributor": mocker.patch("apps.github.common.RepositoryContributor"), + "ActivityEvent": mocker.patch("apps.github.common.ActivityEvent"), "check_owasp": mocker.patch( "apps.github.common.check_owasp_site_repository", return_value=False ), From 3758d68167c6938b9912547c2400c115f9dd180b Mon Sep 17 00:00:00 2001 From: Anurag Yadav Date: Thu, 30 Jul 2026 00:37:26 +0530 Subject: [PATCH 4/9] Address review Signed-off-by: Anurag Yadav --- .../owasp/migrations/0073_activityevent.py | 98 +++++++++++++++---- .../src/apps/owasp/models/activity_event.py | 3 +- docker-compose/local/compose.override.yaml | 13 +++ docker-compose/local/compose.yaml | 10 +- 4 files changed, 100 insertions(+), 24 deletions(-) diff --git a/backend/src/apps/owasp/migrations/0073_activityevent.py b/backend/src/apps/owasp/migrations/0073_activityevent.py index f5cf1fbc03..ba75297650 100644 --- a/backend/src/apps/owasp/migrations/0073_activityevent.py +++ b/backend/src/apps/owasp/migrations/0073_activityevent.py @@ -5,32 +5,94 @@ class Migration(migrations.Migration): - dependencies = [ - ('contenttypes', '0002_remove_content_type_name'), - ('github', '0044_user_indexes'), - ('owasp', '0072_project_project_name_gin_idx_and_more'), + ("contenttypes", "0002_remove_content_type_name"), + ("github", "0044_user_indexes"), + ("owasp", "0072_project_project_name_gin_idx_and_more"), ] operations = [ migrations.CreateModel( - name='ActivityEvent', + 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')), + ( + "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')], + "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", + ) + ], }, ), ] diff --git a/backend/src/apps/owasp/models/activity_event.py b/backend/src/apps/owasp/models/activity_event.py index 79b2194ffd..3db60269c3 100644 --- a/backend/src/apps/owasp/models/activity_event.py +++ b/backend/src/apps/owasp/models/activity_event.py @@ -131,7 +131,8 @@ def update_data(obj) -> None: "ActivityEvent.update_data received unsupported model type: %s", type(obj).__name__, ) - raise TypeError(f"Unsupported model type: {type(obj)}") + message = f"Unsupported model type: {type(obj)}" + raise TypeError(message) handler = getattr(ActivityEvent, handler_name) events = handler(obj) diff --git a/docker-compose/local/compose.override.yaml b/docker-compose/local/compose.override.yaml index e69de29bb2..25ce0eb8cb 100644 --- a/docker-compose/local/compose.override.yaml +++ b/docker-compose/local/compose.override.yaml @@ -0,0 +1,13 @@ +volumes: + backend-venv: + name: backend-venv-pulse + cache-data: + name: cache-data-pulse + db-data: + name: db-data-pulse + docs-venv: + name: docs-venv-pulse + frontend-next: + name: frontend-next-pulse + frontend-node-modules: + name: frontend-node-modules-pulse diff --git a/docker-compose/local/compose.yaml b/docker-compose/local/compose.yaml index e664a4b493..a76bb34021 100644 --- a/docker-compose/local/compose.yaml +++ b/docker-compose/local/compose.yaml @@ -23,7 +23,7 @@ services: - 8000:8000 volumes: - ../../backend:/home/owasp - - backend-venv-pulse:/home/owasp/.venv + - backend-venv:/home/owasp/.venv cache: command: > @@ -41,7 +41,7 @@ services: networks: - nest-network volumes: - - cache-data-pulse:/data + - cache-data:/data db: container_name: nest-db @@ -97,8 +97,8 @@ services: - 3000:3000 volumes: - ../../frontend:/home/owasp - - frontend-next-pulse:/home/owasp/.next - - frontend-node-modules-pulse:/home/owasp/node_modules + - frontend-next:/home/owasp/.next + - frontend-node-modules:/home/owasp/node_modules worker: container_name: nest-worker @@ -119,7 +119,7 @@ services: - nest-network volumes: - ../../backend:/home/owasp - - backend-venv-pulse:/home/owasp/.venv + - backend-venv:/home/owasp/.venv networks: nest-network: From 5244db5f53109de7e6c1b6b27c9a2bdfca2f038d Mon Sep 17 00:00:00 2001 From: Anurag Yadav Date: Thu, 30 Jul 2026 01:07:13 +0530 Subject: [PATCH 5/9] updated order Signed-off-by: Anurag Yadav --- backend/src/apps/owasp/admin/activity_event.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/apps/owasp/admin/activity_event.py b/backend/src/apps/owasp/admin/activity_event.py index 72da546259..6a16479878 100644 --- a/backend/src/apps/owasp/admin/activity_event.py +++ b/backend/src/apps/owasp/admin/activity_event.py @@ -14,9 +14,9 @@ class ActivityEventAdmin(admin.ModelAdmin): ) list_display = ( "activity_type", + "github_repository", "github_user", "occurred_at", - "github_repository", ) list_filter = ( "activity_type", @@ -24,8 +24,8 @@ class ActivityEventAdmin(admin.ModelAdmin): ) search_fields = ( "activity_type", - "github_user__login", "github_repository__name", + "github_user__login", ) From 5264756d78efd49ef8d6ea89645f18c2f9046f05 Mon Sep 17 00:00:00 2001 From: Anurag Yadav Date: Sun, 9 Aug 2026 13:43:16 +0530 Subject: [PATCH 6/9] Fixed Published Issue Signed-off-by: Anurag Yadav --- backend/src/apps/github/common.py | 11 +++++++---- backend/src/apps/owasp/models/activity_event.py | 6 ++++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/backend/src/apps/github/common.py b/backend/src/apps/github/common.py index eb0abfe3d7..2473fcef25 100644 --- a/backend/src/apps/github/common.py +++ b/backend/src/apps/github/common.py @@ -208,7 +208,10 @@ def sync_repository( releases = [] if not is_owasp_site_repository: existing_release_node_ids = set( - Release.objects.filter(repository=repository).values_list("node_id", flat=True) + Release.objects.filter( + repository=repository, + published_at__isnull=False, + ).values_list("node_id", flat=True) if repository.id else () ) @@ -218,10 +221,10 @@ def sync_repository( break author = User.update_data(gh_release.author) - releases.append(Release.update_data(gh_release, author=author, repository=repository)) + release = Release.update_data(gh_release, author=author, repository=repository) + releases.append(release) + ActivityEvent.update_data(release) Release.bulk_save(releases) - for release in releases: - ActivityEvent.update_data(release) # GitHub repository contributors. RepositoryContributor.bulk_save( diff --git a/backend/src/apps/owasp/models/activity_event.py b/backend/src/apps/owasp/models/activity_event.py index 3db60269c3..8873a33a25 100644 --- a/backend/src/apps/owasp/models/activity_event.py +++ b/backend/src/apps/owasp/models/activity_event.py @@ -119,8 +119,10 @@ def build_for_pull_request(pr) -> list[tuple]: @staticmethod def build_for_release(release) -> list[tuple]: """Return event tuples for a Release.""" - occurred_at = release.published_at or release.created_at - return [(ActivityEvent.ActivityType.RELEASE_PUBLISHED, occurred_at, release.author)] + if release.published_at is None: + return [] + + return [(ActivityEvent.ActivityType.RELEASE_PUBLISHED, release.published_at, release.author)] @staticmethod def update_data(obj) -> None: From 80e1c5fab791259df563a945058e8517acd6c71b Mon Sep 17 00:00:00 2001 From: Anurag Yadav Date: Sun, 9 Aug 2026 16:54:09 +0530 Subject: [PATCH 7/9] adress review Signed-off-by: Anurag Yadav --- backend/src/apps/github/common.py | 10 ++++-- .../src/apps/owasp/models/activity_event.py | 31 ++++++++++++------- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/backend/src/apps/github/common.py b/backend/src/apps/github/common.py index 2473fcef25..405b78a813 100644 --- a/backend/src/apps/github/common.py +++ b/backend/src/apps/github/common.py @@ -116,6 +116,7 @@ def sync_repository( if (latest_updated_issue := repository.latest_updated_issue) else month_ago ) + issues = [] for gh_issue in gh_repository.get_issues(**kwargs): if gh_issue.pull_request: # Skip pull requests. continue @@ -139,7 +140,7 @@ def sync_repository( milestone=milestone, repository=repository, ) - ActivityEvent.update_data(issue) + issues.append(issue) # Assignees. issue.assignees.clear() @@ -154,6 +155,7 @@ def sync_repository( issue.labels.add(Label.update_data(gh_issue_label)) except UnknownObjectException: logger.exception("Couldn't get GitHub issue label %s", issue.url) + ActivityEvent.bulk_save_for_objects(issues) else: logger.info("Skipping issues sync for %s", repository.name) @@ -168,6 +170,7 @@ def sync_repository( if (latest_updated_pull_request := repository.latest_updated_pull_request) else month_ago ) + pull_requests = [] for gh_pull_request in gh_repository.get_pulls(**kwargs): if gh_pull_request.updated_at < until: break @@ -188,7 +191,7 @@ def sync_repository( milestone=milestone, repository=repository, ) - ActivityEvent.update_data(pull_request) + pull_requests.append(pull_request) # Assignees. pull_request.assignees.clear() @@ -203,6 +206,7 @@ def sync_repository( pull_request.labels.add(Label.update_data(gh_pull_request_label)) except UnknownObjectException: logger.exception("Couldn't get GitHub pull request label %s", pull_request.url) + ActivityEvent.bulk_save_for_objects(pull_requests) # GitHub repository releases. releases = [] @@ -223,8 +227,8 @@ def sync_repository( author = User.update_data(gh_release.author) release = Release.update_data(gh_release, author=author, repository=repository) releases.append(release) - ActivityEvent.update_data(release) Release.bulk_save(releases) + ActivityEvent.bulk_save_for_objects(releases) # GitHub repository contributors. RepositoryContributor.bulk_save( diff --git a/backend/src/apps/owasp/models/activity_event.py b/backend/src/apps/owasp/models/activity_event.py index 8873a33a25..4a4b2b5777 100644 --- a/backend/src/apps/owasp/models/activity_event.py +++ b/backend/src/apps/owasp/models/activity_event.py @@ -122,11 +122,13 @@ def build_for_release(release) -> list[tuple]: if release.published_at is None: return [] - return [(ActivityEvent.ActivityType.RELEASE_PUBLISHED, release.published_at, release.author)] + return [ + (ActivityEvent.ActivityType.RELEASE_PUBLISHED, release.published_at, release.author) + ] @staticmethod - def update_data(obj) -> None: - """Create ActivityEvent row(s) for a saved GitHub model instance if they do not exist.""" + def update_data(obj) -> list["ActivityEvent"]: + """Return unsaved ActivityEvent instances for a GitHub model object.""" handler_name = ActivityEvent.HANDLERS.get(type(obj).__name__) if handler_name is None: logger.error( @@ -140,17 +142,22 @@ def update_data(obj) -> None: events = handler(obj) content_type = ContentType.objects.get_for_model(obj) - for activity_type, occurred_at, github_user in events: - if occurred_at is None: - continue - - ActivityEvent.objects.get_or_create( + return [ + ActivityEvent( activity_type=activity_type, content_type=content_type, object_id=obj.pk, occurred_at=occurred_at, - defaults={ - "github_user": github_user, - "github_repository": obj.repository, - }, + github_user=github_user, + github_repository=obj.repository, ) + for activity_type, occurred_at, github_user in events + if occurred_at is not None + ] + + @staticmethod + def bulk_save_for_objects(objects: list) -> None: + """Bulk-insert ActivityEvent rows for source objects, skipping duplicates.""" + events = [event for obj in objects for event in ActivityEvent.update_data(obj)] + if events: + ActivityEvent.objects.bulk_create(events, ignore_conflicts=True) From 19a7374052366ed7d36b009d1ac50f151e032438 Mon Sep 17 00:00:00 2001 From: Anurag Yadav Date: Sun, 9 Aug 2026 17:00:01 +0530 Subject: [PATCH 8/9] update Signed-off-by: Anurag Yadav --- backend/src/apps/github/common.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/src/apps/github/common.py b/backend/src/apps/github/common.py index 405b78a813..e76615a5ec 100644 --- a/backend/src/apps/github/common.py +++ b/backend/src/apps/github/common.py @@ -227,8 +227,9 @@ def sync_repository( author = User.update_data(gh_release.author) release = Release.update_data(gh_release, author=author, repository=repository) releases.append(release) + releases_for_events = list(releases) Release.bulk_save(releases) - ActivityEvent.bulk_save_for_objects(releases) + ActivityEvent.bulk_save_for_objects(releases_for_events) # GitHub repository contributors. RepositoryContributor.bulk_save( From 023e9b4e7f1fde5f5fcffb040feca91df60e059a Mon Sep 17 00:00:00 2001 From: Anurag Yadav Date: Wed, 12 Aug 2026 21:48:43 +0530 Subject: [PATCH 9/9] fixed review Signed-off-by: Anurag Yadav --- backend/src/apps/github/common.py | 6 ++--- .../src/apps/owasp/models/activity_event.py | 25 ++++++++++--------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/backend/src/apps/github/common.py b/backend/src/apps/github/common.py index e76615a5ec..56f1248ddc 100644 --- a/backend/src/apps/github/common.py +++ b/backend/src/apps/github/common.py @@ -155,7 +155,7 @@ def sync_repository( issue.labels.add(Label.update_data(gh_issue_label)) except UnknownObjectException: logger.exception("Couldn't get GitHub issue label %s", issue.url) - ActivityEvent.bulk_save_for_objects(issues) + ActivityEvent.bulk_save_for_sources(issues) else: logger.info("Skipping issues sync for %s", repository.name) @@ -206,7 +206,7 @@ def sync_repository( pull_request.labels.add(Label.update_data(gh_pull_request_label)) except UnknownObjectException: logger.exception("Couldn't get GitHub pull request label %s", pull_request.url) - ActivityEvent.bulk_save_for_objects(pull_requests) + ActivityEvent.bulk_save_for_sources(pull_requests) # GitHub repository releases. releases = [] @@ -229,7 +229,7 @@ def sync_repository( releases.append(release) releases_for_events = list(releases) Release.bulk_save(releases) - ActivityEvent.bulk_save_for_objects(releases_for_events) + ActivityEvent.bulk_save_for_sources(releases_for_events) # GitHub repository contributors. RepositoryContributor.bulk_save( diff --git a/backend/src/apps/owasp/models/activity_event.py b/backend/src/apps/owasp/models/activity_event.py index 4a4b2b5777..ed5d307bca 100644 --- a/backend/src/apps/owasp/models/activity_event.py +++ b/backend/src/apps/owasp/models/activity_event.py @@ -7,6 +7,7 @@ from django.db import models from apps.common.models import BulkSaveModel, TimestampedModel +from apps.github.models.generic_issue_model import GenericIssueModel logger = logging.getLogger(__name__) @@ -102,7 +103,7 @@ def bulk_save(activity_events, fields=None) -> None: # type: ignore[override] def build_for_issue(issue) -> list[tuple]: """Return event tuples for an Issue.""" events = [(ActivityEvent.ActivityType.ISSUE_OPENED, issue.created_at, issue.author)] - if issue.state == "closed" and issue.closed_at: + if issue.state == GenericIssueModel.IssueState.CLOSED and issue.closed_at: events.append((ActivityEvent.ActivityType.ISSUE_CLOSED, issue.closed_at, issue.author)) return events @@ -112,7 +113,7 @@ def build_for_pull_request(pr) -> list[tuple]: events = [(ActivityEvent.ActivityType.PR_OPENED, pr.created_at, pr.author)] if pr.merged_at: events.append((ActivityEvent.ActivityType.PR_MERGED, pr.merged_at, pr.author)) - elif pr.state == "closed" and pr.closed_at: + elif pr.state == GenericIssueModel.IssueState.CLOSED and pr.closed_at: events.append((ActivityEvent.ActivityType.PR_CLOSED, pr.closed_at, pr.author)) return events @@ -127,37 +128,37 @@ def build_for_release(release) -> list[tuple]: ] @staticmethod - def update_data(obj) -> list["ActivityEvent"]: + def update_data(source) -> list["ActivityEvent"]: """Return unsaved ActivityEvent instances for a GitHub model object.""" - handler_name = ActivityEvent.HANDLERS.get(type(obj).__name__) + handler_name = ActivityEvent.HANDLERS.get(type(source).__name__) if handler_name is None: logger.error( "ActivityEvent.update_data received unsupported model type: %s", - type(obj).__name__, + type(source).__name__, ) - message = f"Unsupported model type: {type(obj)}" + message = f"Unsupported model type: {type(source)}" raise TypeError(message) handler = getattr(ActivityEvent, handler_name) - events = handler(obj) - content_type = ContentType.objects.get_for_model(obj) + events = handler(source) + content_type = ContentType.objects.get_for_model(source) return [ ActivityEvent( activity_type=activity_type, content_type=content_type, - object_id=obj.pk, + object_id=source.pk, occurred_at=occurred_at, github_user=github_user, - github_repository=obj.repository, + github_repository=source.repository, ) for activity_type, occurred_at, github_user in events if occurred_at is not None ] @staticmethod - def bulk_save_for_objects(objects: list) -> None: + def bulk_save_for_sources(sources: list) -> None: """Bulk-insert ActivityEvent rows for source objects, skipping duplicates.""" - events = [event for obj in objects for event in ActivityEvent.update_data(obj)] + events = [event for source in sources for event in ActivityEvent.update_data(source)] if events: ActivityEvent.objects.bulk_create(events, ignore_conflicts=True)