From fe55a2692b2b505b8ab15caa4a03a880bdf818fc Mon Sep 17 00:00:00 2001 From: anurag2787 Date: Tue, 12 Aug 2025 01:15:18 +0530 Subject: [PATCH 1/6] link OWASP entities to related Slack channels --- backend/apps/slack/admin/__init__.py | 1 + backend/apps/slack/admin/entity_channel.py | 14 ++++++ .../commands/owasp_match_channels.py | 36 +++++++++++++ .../slack/migrations/0019_entitychannel.py | 50 +++++++++++++++++++ ..._entitychannel_nest_created_at_and_more.py | 21 ++++++++ backend/apps/slack/models/__init__.py | 1 + backend/apps/slack/models/entity_channel.py | 21 ++++++++ 7 files changed, 144 insertions(+) create mode 100644 backend/apps/slack/admin/entity_channel.py create mode 100644 backend/apps/slack/management/commands/owasp_match_channels.py create mode 100644 backend/apps/slack/migrations/0019_entitychannel.py create mode 100644 backend/apps/slack/migrations/0020_remove_entitychannel_nest_created_at_and_more.py create mode 100644 backend/apps/slack/models/entity_channel.py diff --git a/backend/apps/slack/admin/__init__.py b/backend/apps/slack/admin/__init__.py index b3193ffb04..14cf601bfc 100644 --- a/backend/apps/slack/admin/__init__.py +++ b/backend/apps/slack/admin/__init__.py @@ -1,6 +1,7 @@ """Slack app admin.""" from .conversation import ConversationAdmin +from .entity_channel import EntityChannelAdmin from .event import EventAdmin from .member import MemberAdmin from .message import MessageAdmin diff --git a/backend/apps/slack/admin/entity_channel.py b/backend/apps/slack/admin/entity_channel.py new file mode 100644 index 0000000000..ee3e1d028c --- /dev/null +++ b/backend/apps/slack/admin/entity_channel.py @@ -0,0 +1,14 @@ +from django.contrib import admin +from django.contrib.contenttypes.admin import GenericTabularInline +from ..models import EntityChannel + +@admin.action(description="Mark selected EntityChannels as reviewed") +def mark_as_reviewed(modeladmin, request, queryset): + queryset.update(is_reviewed=True) + +@admin.register(EntityChannel) +class EntityChannelAdmin(admin.ModelAdmin): + list_display = ("entity", "conversation", "is_main_channel", "is_reviewed", "kind") + list_filter = ("is_main_channel", "is_reviewed", "kind", "content_type") + search_fields = ("object_id", "conversation__name") + actions = [mark_as_reviewed] diff --git a/backend/apps/slack/management/commands/owasp_match_channels.py b/backend/apps/slack/management/commands/owasp_match_channels.py new file mode 100644 index 0000000000..1975eb3eb1 --- /dev/null +++ b/backend/apps/slack/management/commands/owasp_match_channels.py @@ -0,0 +1,36 @@ +from django.core.management.base import BaseCommand +from django.contrib.contenttypes.models import ContentType +from apps.slack.models import Conversation, EntityChannel +from apps.owasp.models.chapter import Chapter +from apps.owasp.models.committee import Committee +from apps.owasp.models.project import Project + +class Command(BaseCommand): + help = 'Populate EntityChannel records for Chapters, Committees, and Projects based on Slack data.' + + def handle(self, *args, **options): + created = 0 + # Example: match by name, can be improved + for model, app_label, model_name in [ + (Chapter, 'owasp', 'chapter'), + (Committee, 'owasp', 'committee'), + (Project, 'owasp', 'project'), + ]: + content_type = ContentType.objects.get(app_label=app_label, model=model_name) + for entity in model.objects.all(): + # Example: match conversation by name + conversations = Conversation.objects.filter(name__icontains=entity.name) + for conv in conversations: + obj, was_created = EntityChannel.objects.get_or_create( + content_type=content_type, + object_id=entity.pk, + conversation=conv, + defaults={ + 'is_main_channel': False, + 'is_reviewed': False, + 'kind': 'slack', + } + ) + if was_created: + created += 1 + self.stdout.write(self.style.SUCCESS(f'Created {created} EntityChannel records.')) diff --git a/backend/apps/slack/migrations/0019_entitychannel.py b/backend/apps/slack/migrations/0019_entitychannel.py new file mode 100644 index 0000000000..7e39b71939 --- /dev/null +++ b/backend/apps/slack/migrations/0019_entitychannel.py @@ -0,0 +1,50 @@ +# Generated by Django for EntityChannel model + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + ('slack', '0018_conversation_sync_messages'), + ('contenttypes', '0002_remove_content_type_name'), + ] + + operations = [ + migrations.CreateModel( + name='EntityChannel', + 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)), + ('object_id', models.PositiveIntegerField()), + ('is_main_channel', models.BooleanField(default=False)), + ('is_reviewed', models.BooleanField(default=False)), + ('kind', models.CharField(default='slack', max_length=32)), + ( + 'content_type', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to='contenttypes.contenttype' + ), + ), + ( + 'conversation', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to='slack.conversation' + ), + ), + ], + options={ + 'unique_together': {('content_type', 'object_id', 'conversation')}, + 'verbose_name': 'Entity Channel', + 'verbose_name_plural': 'Entity Channels', + }, + ), + ] diff --git a/backend/apps/slack/migrations/0020_remove_entitychannel_nest_created_at_and_more.py b/backend/apps/slack/migrations/0020_remove_entitychannel_nest_created_at_and_more.py new file mode 100644 index 0000000000..09b9a614ce --- /dev/null +++ b/backend/apps/slack/migrations/0020_remove_entitychannel_nest_created_at_and_more.py @@ -0,0 +1,21 @@ +# Generated by Django 5.2.4 on 2025-08-11 18:01 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('slack', '0019_entitychannel'), + ] + + operations = [ + migrations.RemoveField( + model_name='entitychannel', + name='nest_created_at', + ), + migrations.RemoveField( + model_name='entitychannel', + name='nest_updated_at', + ), + ] diff --git a/backend/apps/slack/models/__init__.py b/backend/apps/slack/models/__init__.py index 3bbe0878de..7a0db8c927 100644 --- a/backend/apps/slack/models/__init__.py +++ b/backend/apps/slack/models/__init__.py @@ -1,4 +1,5 @@ from .conversation import Conversation +from .entity_channel import EntityChannel from .event import Event from .member import Member from .message import Message diff --git a/backend/apps/slack/models/entity_channel.py b/backend/apps/slack/models/entity_channel.py new file mode 100644 index 0000000000..323bceda31 --- /dev/null +++ b/backend/apps/slack/models/entity_channel.py @@ -0,0 +1,21 @@ +from django.db import models +from django.contrib.contenttypes.fields import GenericForeignKey +from django.contrib.contenttypes.models import ContentType +from .conversation import Conversation + +class EntityChannel(models.Model): + content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) + object_id = models.PositiveIntegerField() + entity = GenericForeignKey('content_type', 'object_id') + conversation = models.ForeignKey(Conversation, on_delete=models.CASCADE) + is_main_channel = models.BooleanField(default=False) + is_reviewed = models.BooleanField(default=False) + kind = models.CharField(max_length=32, default='slack') + + class Meta: + unique_together = ('content_type', 'object_id', 'conversation') + verbose_name = 'Entity Channel' + verbose_name_plural = 'Entity Channels' + + def __str__(self): + return f"{self.entity} - {self.conversation} ({self.kind})" From b91fa149f12bee7987db7d2815aaa9a9c2c71c6d Mon Sep 17 00:00:00 2001 From: anurag2787 Date: Tue, 12 Aug 2025 10:01:08 +0530 Subject: [PATCH 2/6] Fixed coderabbitai review --- backend/apps/slack/admin/entity_channel.py | 10 ++++-- .../commands/owasp_match_channels.py | 31 +++++++++++-------- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/backend/apps/slack/admin/entity_channel.py b/backend/apps/slack/admin/entity_channel.py index ee3e1d028c..593b205c37 100644 --- a/backend/apps/slack/admin/entity_channel.py +++ b/backend/apps/slack/admin/entity_channel.py @@ -1,10 +1,14 @@ from django.contrib import admin -from django.contrib.contenttypes.admin import GenericTabularInline from ..models import EntityChannel @admin.action(description="Mark selected EntityChannels as reviewed") -def mark_as_reviewed(modeladmin, request, queryset): - queryset.update(is_reviewed=True) +def mark_as_reviewed(_modeladmin, request, queryset): + updated = queryset.update(is_reviewed=True) + # Provide feedback in the admin UI + request._messages.add( + level=20, + message=f"Marked {updated} EntityChannel(s) as reviewed.", + ) @admin.register(EntityChannel) class EntityChannelAdmin(admin.ModelAdmin): diff --git a/backend/apps/slack/management/commands/owasp_match_channels.py b/backend/apps/slack/management/commands/owasp_match_channels.py index 1975eb3eb1..ead551ba92 100644 --- a/backend/apps/slack/management/commands/owasp_match_channels.py +++ b/backend/apps/slack/management/commands/owasp_match_channels.py @@ -5,30 +5,35 @@ from apps.owasp.models.committee import Committee from apps.owasp.models.project import Project +ct = ContentType.objects.get_for_model(Chapter) + class Command(BaseCommand): help = 'Populate EntityChannel records for Chapters, Committees, and Projects based on Slack data.' def handle(self, *args, **options): + from django.utils.text import slugify created = 0 - # Example: match by name, can be improved - for model, app_label, model_name in [ - (Chapter, 'owasp', 'chapter'), - (Committee, 'owasp', 'committee'), - (Project, 'owasp', 'project'), - ]: - content_type = ContentType.objects.get(app_label=app_label, model=model_name) - for entity in model.objects.all(): - # Example: match conversation by name - conversations = Conversation.objects.filter(name__icontains=entity.name) + for model in (Chapter, Committee, Project): + content_type = ContentType.objects.get_for_model(model) + # Use .only and .iterator for memory efficiency + for entity in model.objects.all().only("id", "name").iterator(): + # Normalize the name for matching (e.g., "OWASP Lima" -> "owasp-lima") + needle = slugify(entity.name or "") + if not needle: + continue + qs = Conversation.objects.all() + # If you add --workspace-id, filter here: + # qs = qs.filter(workspace_id=options.get("workspace_id")) + conversations = qs.filter(name__icontains=needle) for conv in conversations: obj, was_created = EntityChannel.objects.get_or_create( content_type=content_type, object_id=entity.pk, conversation=conv, defaults={ - 'is_main_channel': False, - 'is_reviewed': False, - 'kind': 'slack', + "is_main_channel": False, + "is_reviewed": False, + "kind": "slack", } ) if was_created: From af6e4fe2b8f77e503dc4d474668ab0b29133fc9f Mon Sep 17 00:00:00 2001 From: anurag2787 Date: Tue, 12 Aug 2025 14:23:42 +0530 Subject: [PATCH 3/6] clean imports and remove unused code --- backend/apps/slack/admin/entity_channel.py | 17 +++-- .../commands/owasp_match_channels.py | 69 +++++++++++-------- .../slack/migrations/0019_entitychannel.py | 40 +++++------ ..._entitychannel_nest_created_at_and_more.py | 11 ++- backend/apps/slack/models/entity_channel.py | 19 +++-- 5 files changed, 87 insertions(+), 69 deletions(-) diff --git a/backend/apps/slack/admin/entity_channel.py b/backend/apps/slack/admin/entity_channel.py index 593b205c37..0fd5ab9c52 100644 --- a/backend/apps/slack/admin/entity_channel.py +++ b/backend/apps/slack/admin/entity_channel.py @@ -1,17 +1,22 @@ -from django.contrib import admin -from ..models import EntityChannel +"""Admin configuration for the EntityChannel model.""" + +from django.contrib import admin, messages + +from apps.slack.models import EntityChannel + @admin.action(description="Mark selected EntityChannels as reviewed") def mark_as_reviewed(_modeladmin, request, queryset): + """Admin action to mark selected EntityChannels as reviewed.""" updated = queryset.update(is_reviewed=True) # Provide feedback in the admin UI - request._messages.add( - level=20, - message=f"Marked {updated} EntityChannel(s) as reviewed.", - ) + messages.success(request, f"Marked {updated} EntityChannel(s) as reviewed.") + @admin.register(EntityChannel) class EntityChannelAdmin(admin.ModelAdmin): + """Admin interface for the EntityChannel model.""" + list_display = ("entity", "conversation", "is_main_channel", "is_reviewed", "kind") list_filter = ("is_main_channel", "is_reviewed", "kind", "content_type") search_fields = ("object_id", "conversation__name") diff --git a/backend/apps/slack/management/commands/owasp_match_channels.py b/backend/apps/slack/management/commands/owasp_match_channels.py index ead551ba92..6d0efc8643 100644 --- a/backend/apps/slack/management/commands/owasp_match_channels.py +++ b/backend/apps/slack/management/commands/owasp_match_channels.py @@ -1,41 +1,50 @@ -from django.core.management.base import BaseCommand +"""Management command to populate EntityChannel records from Slack data.""" + from django.contrib.contenttypes.models import ContentType -from apps.slack.models import Conversation, EntityChannel +from django.core.management.base import BaseCommand +from django.db import transaction +from django.db.models import Q +from django.utils.text import slugify + from apps.owasp.models.chapter import Chapter from apps.owasp.models.committee import Committee from apps.owasp.models.project import Project +from apps.slack.models import Conversation, EntityChannel -ct = ContentType.objects.get_for_model(Chapter) class Command(BaseCommand): - help = 'Populate EntityChannel records for Chapters, Committees, and Projects based on Slack data.' + help = "Populate EntityChannel links for Chapters, Committees, and Projects." def handle(self, *args, **options): - from django.utils.text import slugify created = 0 - for model in (Chapter, Committee, Project): - content_type = ContentType.objects.get_for_model(model) - # Use .only and .iterator for memory efficiency - for entity in model.objects.all().only("id", "name").iterator(): - # Normalize the name for matching (e.g., "OWASP Lima" -> "owasp-lima") - needle = slugify(entity.name or "") - if not needle: - continue - qs = Conversation.objects.all() - # If you add --workspace-id, filter here: - # qs = qs.filter(workspace_id=options.get("workspace_id")) - conversations = qs.filter(name__icontains=needle) - for conv in conversations: - obj, was_created = EntityChannel.objects.get_or_create( - content_type=content_type, - object_id=entity.pk, - conversation=conv, - defaults={ - "is_main_channel": False, - "is_reviewed": False, - "kind": "slack", - } + with transaction.atomic(): + for model in (Chapter, Committee, Project): + content_type = ContentType.objects.get_for_model(model) + # Use .only and .iterator for memory efficiency + for entity in model.objects.all().only("id", "name").iterator(): + # Normalize the name for matching (e.g., "OWASP Lima" -> "owasp-lima") + needle = slugify(entity.name or "") + if not needle: + continue + qs = Conversation.objects.all() + workspace_id = options.get("workspace_id") + if workspace_id: + qs = qs.filter(workspace_id=workspace_id) + alt_needle = needle.replace("-", "_") + conversations = qs.filter( + Q(name__icontains=needle) | Q(name__icontains=alt_needle) ) - if was_created: - created += 1 - self.stdout.write(self.style.SUCCESS(f'Created {created} EntityChannel records.')) + for conv in conversations: + _, was_created = EntityChannel.objects.get_or_create( + content_type=content_type, + object_id=entity.pk, + conversation=conv, + defaults={ + "is_main_channel": False, + "is_reviewed": False, + "kind": "slack", + }, + ) + if was_created: + created += 1 + self.stdout.write(self.style.SUCCESS(f"Created {created} EntityChannel records.")) diff --git a/backend/apps/slack/migrations/0019_entitychannel.py b/backend/apps/slack/migrations/0019_entitychannel.py index 7e39b71939..f1e4c0e321 100644 --- a/backend/apps/slack/migrations/0019_entitychannel.py +++ b/backend/apps/slack/migrations/0019_entitychannel.py @@ -1,50 +1,48 @@ # Generated by Django for EntityChannel model -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('slack', '0018_conversation_sync_messages'), - ('contenttypes', '0002_remove_content_type_name'), + ("slack", "0018_conversation_sync_messages"), + ("contenttypes", "0002_remove_content_type_name"), ] operations = [ migrations.CreateModel( - name='EntityChannel', + name="EntityChannel", fields=[ ( - 'id', + "id", models.BigAutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name='ID' + 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)), - ('object_id', models.PositiveIntegerField()), - ('is_main_channel', models.BooleanField(default=False)), - ('is_reviewed', models.BooleanField(default=False)), - ('kind', models.CharField(default='slack', max_length=32)), + ("nest_created_at", models.DateTimeField(auto_now_add=True)), + ("nest_updated_at", models.DateTimeField(auto_now=True)), + ("object_id", models.PositiveIntegerField()), + ("is_main_channel", models.BooleanField(default=False)), + ("is_reviewed", models.BooleanField(default=False)), + ("kind", models.CharField(default="slack", max_length=32)), ( - 'content_type', + "content_type", models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - to='contenttypes.contenttype' + on_delete=django.db.models.deletion.CASCADE, to="contenttypes.contenttype" ), ), ( - 'conversation', + "conversation", models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - to='slack.conversation' + on_delete=django.db.models.deletion.CASCADE, to="slack.conversation" ), ), ], options={ - 'unique_together': {('content_type', 'object_id', 'conversation')}, - 'verbose_name': 'Entity Channel', - 'verbose_name_plural': 'Entity Channels', + "unique_together": {("content_type", "object_id", "conversation")}, + "verbose_name": "Entity Channel", + "verbose_name_plural": "Entity Channels", }, ), ] diff --git a/backend/apps/slack/migrations/0020_remove_entitychannel_nest_created_at_and_more.py b/backend/apps/slack/migrations/0020_remove_entitychannel_nest_created_at_and_more.py index 09b9a614ce..1d64507332 100644 --- a/backend/apps/slack/migrations/0020_remove_entitychannel_nest_created_at_and_more.py +++ b/backend/apps/slack/migrations/0020_remove_entitychannel_nest_created_at_and_more.py @@ -4,18 +4,17 @@ class Migration(migrations.Migration): - dependencies = [ - ('slack', '0019_entitychannel'), + ("slack", "0019_entitychannel"), ] operations = [ migrations.RemoveField( - model_name='entitychannel', - name='nest_created_at', + model_name="entitychannel", + name="nest_created_at", ), migrations.RemoveField( - model_name='entitychannel', - name='nest_updated_at', + model_name="entitychannel", + name="nest_updated_at", ), ] diff --git a/backend/apps/slack/models/entity_channel.py b/backend/apps/slack/models/entity_channel.py index 323bceda31..8fe809e56e 100644 --- a/backend/apps/slack/models/entity_channel.py +++ b/backend/apps/slack/models/entity_channel.py @@ -1,21 +1,28 @@ -from django.db import models +"""Model for linking entities (chapter, committee, project) to Slack conversations.""" + from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType +from django.db import models + from .conversation import Conversation + class EntityChannel(models.Model): + """Model representing a link between an entity and a Slack conversation.""" + content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) object_id = models.PositiveIntegerField() - entity = GenericForeignKey('content_type', 'object_id') + entity = GenericForeignKey("content_type", "object_id") conversation = models.ForeignKey(Conversation, on_delete=models.CASCADE) is_main_channel = models.BooleanField(default=False) is_reviewed = models.BooleanField(default=False) - kind = models.CharField(max_length=32, default='slack') + kind = models.CharField(max_length=32, default="slack") class Meta: - unique_together = ('content_type', 'object_id', 'conversation') - verbose_name = 'Entity Channel' - verbose_name_plural = 'Entity Channels' + unique_together = ("content_type", "object_id", "conversation") + verbose_name = "Entity Channel" + verbose_name_plural = "Entity Channels" def __str__(self): + """Return a readable string representation of the EntityChannel.""" return f"{self.entity} - {self.conversation} ({self.kind})" From 7151bae95122ef8ef1410697adfabc63a49239a3 Mon Sep 17 00:00:00 2001 From: anurag2787 Date: Tue, 12 Aug 2025 16:08:53 +0530 Subject: [PATCH 4/6] update command options and filters --- .../commands/owasp_match_channels.py | 55 ++++++++----------- 1 file changed, 23 insertions(+), 32 deletions(-) diff --git a/backend/apps/slack/management/commands/owasp_match_channels.py b/backend/apps/slack/management/commands/owasp_match_channels.py index 6d0efc8643..ac121f4c79 100644 --- a/backend/apps/slack/management/commands/owasp_match_channels.py +++ b/backend/apps/slack/management/commands/owasp_match_channels.py @@ -1,9 +1,7 @@ -"""Management command to populate EntityChannel records from Slack data.""" +"""A command to populate EntityChannel records from Slack data.""" from django.contrib.contenttypes.models import ContentType from django.core.management.base import BaseCommand -from django.db import transaction -from django.db.models import Q from django.utils.text import slugify from apps.owasp.models.chapter import Chapter @@ -17,34 +15,27 @@ class Command(BaseCommand): def handle(self, *args, **options): created = 0 - with transaction.atomic(): - for model in (Chapter, Committee, Project): - content_type = ContentType.objects.get_for_model(model) - # Use .only and .iterator for memory efficiency - for entity in model.objects.all().only("id", "name").iterator(): - # Normalize the name for matching (e.g., "OWASP Lima" -> "owasp-lima") - needle = slugify(entity.name or "") - if not needle: - continue - qs = Conversation.objects.all() - workspace_id = options.get("workspace_id") - if workspace_id: - qs = qs.filter(workspace_id=workspace_id) - alt_needle = needle.replace("-", "_") - conversations = qs.filter( - Q(name__icontains=needle) | Q(name__icontains=alt_needle) + for model in (Chapter, Committee, Project): + content_type = ContentType.objects.get_for_model(model) + # Use .only and .iterator for memory efficiency + for entity in model.objects.all().only("id", "name").iterator(): + # Normalize the name for matching (e.g., "OWASP Lima" -> "owasp-lima") + needle = slugify(entity.name or "") + if not needle: + continue + qs = Conversation.objects.all() + conversations = qs.filter(name__icontains=needle) + for conv in conversations: + _, was_created = EntityChannel.objects.get_or_create( + content_type=content_type, + object_id=entity.pk, + conversation=conv, + defaults={ + "is_main_channel": False, + "is_reviewed": False, + "kind": "slack", + }, ) - for conv in conversations: - _, was_created = EntityChannel.objects.get_or_create( - content_type=content_type, - object_id=entity.pk, - conversation=conv, - defaults={ - "is_main_channel": False, - "is_reviewed": False, - "kind": "slack", - }, - ) - if was_created: - created += 1 + if was_created: + created += 1 self.stdout.write(self.style.SUCCESS(f"Created {created} EntityChannel records.")) From bc7b3130dc28fb017c6177395a7deb0d788eb8e7 Mon Sep 17 00:00:00 2001 From: Arkadii Yakovets Date: Fri, 15 Aug 2025 15:38:51 -0700 Subject: [PATCH 5/6] Update code --- backend/apps/slack/admin/entity_channel.py | 27 +++++--- .../slack/migrations/0019_entitychannel.py | 52 +++++++++++----- ..._entitychannel_nest_created_at_and_more.py | 20 ------ backend/apps/slack/models/entity_channel.py | 61 ++++++++++++++----- 4 files changed, 103 insertions(+), 57 deletions(-) delete mode 100644 backend/apps/slack/migrations/0020_remove_entitychannel_nest_created_at_and_more.py diff --git a/backend/apps/slack/admin/entity_channel.py b/backend/apps/slack/admin/entity_channel.py index 0fd5ab9c52..4b114539c1 100644 --- a/backend/apps/slack/admin/entity_channel.py +++ b/backend/apps/slack/admin/entity_channel.py @@ -8,16 +8,29 @@ @admin.action(description="Mark selected EntityChannels as reviewed") def mark_as_reviewed(_modeladmin, request, queryset): """Admin action to mark selected EntityChannels as reviewed.""" - updated = queryset.update(is_reviewed=True) - # Provide feedback in the admin UI - messages.success(request, f"Marked {updated} EntityChannel(s) as reviewed.") + messages.success( + request, + f"Marked {queryset.update(is_reviewed=True)} EntityChannel(s) as reviewed.", + ) @admin.register(EntityChannel) class EntityChannelAdmin(admin.ModelAdmin): """Admin interface for the EntityChannel model.""" - list_display = ("entity", "conversation", "is_main_channel", "is_reviewed", "kind") - list_filter = ("is_main_channel", "is_reviewed", "kind", "content_type") - search_fields = ("object_id", "conversation__name") - actions = [mark_as_reviewed] + actions = (mark_as_reviewed,) + list_display = ( + "entity", + "channel", + "is_default", + "is_reviewed", + "platform", + ) + list_filter = ( + "is_default", + "is_reviewed", + "platform", + "entity_type", + "channel_type", + ) + search_fields = ("channel_id", "entity_id") diff --git a/backend/apps/slack/migrations/0019_entitychannel.py b/backend/apps/slack/migrations/0019_entitychannel.py index f1e4c0e321..97621bbbb6 100644 --- a/backend/apps/slack/migrations/0019_entitychannel.py +++ b/backend/apps/slack/migrations/0019_entitychannel.py @@ -1,4 +1,4 @@ -# Generated by Django for EntityChannel model +# Generated by Django 5.2.5 on 2025-08-15 22:35 import django.db.models.deletion from django.db import migrations, models @@ -6,8 +6,8 @@ class Migration(migrations.Migration): dependencies = [ - ("slack", "0018_conversation_sync_messages"), ("contenttypes", "0002_remove_content_type_name"), + ("slack", "0018_conversation_sync_messages"), ] operations = [ @@ -20,29 +20,51 @@ class Migration(migrations.Migration): 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)), - ("object_id", models.PositiveIntegerField()), - ("is_main_channel", models.BooleanField(default=False)), - ("is_reviewed", models.BooleanField(default=False)), - ("kind", models.CharField(default="slack", max_length=32)), + ("channel_id", models.PositiveIntegerField()), + ("entity_id", models.PositiveIntegerField()), + ( + "is_default", + models.BooleanField( + default=False, + help_text="Indicates if this is the main channel for the entity", + ), + ), + ( + "is_reviewed", + models.BooleanField( + default=False, help_text="Indicates if the channel has been reviewed" + ), + ), + ( + "platform", + models.CharField( + choices=[("slack", "Slack")], + default="slack", + help_text="Platform of the channel (e.g., Slack)", + max_length=5, + ), + ), ( - "content_type", + "channel_type", models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to="contenttypes.contenttype" + on_delete=django.db.models.deletion.CASCADE, + related_name="+", + to="contenttypes.contenttype", ), ), ( - "conversation", + "entity_type", models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to="slack.conversation" + on_delete=django.db.models.deletion.CASCADE, + related_name="+", + to="contenttypes.contenttype", ), ), ], options={ - "unique_together": {("content_type", "object_id", "conversation")}, - "verbose_name": "Entity Channel", - "verbose_name_plural": "Entity Channels", + "verbose_name": "Entity channel", + "verbose_name_plural": "Entity channels", + "unique_together": {("channel_id", "channel_type", "entity_id", "entity_type")}, }, ), ] diff --git a/backend/apps/slack/migrations/0020_remove_entitychannel_nest_created_at_and_more.py b/backend/apps/slack/migrations/0020_remove_entitychannel_nest_created_at_and_more.py deleted file mode 100644 index 1d64507332..0000000000 --- a/backend/apps/slack/migrations/0020_remove_entitychannel_nest_created_at_and_more.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 5.2.4 on 2025-08-11 18:01 - -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [ - ("slack", "0019_entitychannel"), - ] - - operations = [ - migrations.RemoveField( - model_name="entitychannel", - name="nest_created_at", - ), - migrations.RemoveField( - model_name="entitychannel", - name="nest_updated_at", - ), - ] diff --git a/backend/apps/slack/models/entity_channel.py b/backend/apps/slack/models/entity_channel.py index 8fe809e56e..4c83b8bae2 100644 --- a/backend/apps/slack/models/entity_channel.py +++ b/backend/apps/slack/models/entity_channel.py @@ -1,28 +1,59 @@ -"""Model for linking entities (chapter, committee, project) to Slack conversations.""" +"""Model for linking OWASP entities (chapter, committee, project) to communication channels.""" from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.db import models -from .conversation import Conversation - class EntityChannel(models.Model): - """Model representing a link between an entity and a Slack conversation.""" + """Model representing a link between an entity and a channel.""" - content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) - object_id = models.PositiveIntegerField() - entity = GenericForeignKey("content_type", "object_id") - conversation = models.ForeignKey(Conversation, on_delete=models.CASCADE) - is_main_channel = models.BooleanField(default=False) - is_reviewed = models.BooleanField(default=False) - kind = models.CharField(max_length=32, default="slack") + class Platform(models.TextChoices): + SLACK = "slack", "Slack" class Meta: - unique_together = ("content_type", "object_id", "conversation") - verbose_name = "Entity Channel" - verbose_name_plural = "Entity Channels" + unique_together = ( + "channel_id", + "channel_type", + "entity_id", + "entity_type", + ) + verbose_name = "Entity channel" + verbose_name_plural = "Entity channels" + + # Channel. + channel = GenericForeignKey("channel_type", "channel_id") + channel_id = models.PositiveIntegerField() + channel_type = models.ForeignKey( + ContentType, + on_delete=models.CASCADE, + related_name="+", + ) + + # Entity. + entity = GenericForeignKey("entity_type", "entity_id") + entity_id = models.PositiveIntegerField() + entity_type = models.ForeignKey( + ContentType, + on_delete=models.CASCADE, + related_name="+", + ) + + is_default = models.BooleanField( + default=False, + help_text="Indicates if this is the main channel for the entity", + ) + is_reviewed = models.BooleanField( + default=False, + help_text="Indicates if the channel has been reviewed", + ) + platform = models.CharField( + max_length=5, + default=Platform.SLACK, + choices=Platform.choices, + help_text="Platform of the channel (e.g., Slack)", + ) def __str__(self): """Return a readable string representation of the EntityChannel.""" - return f"{self.entity} - {self.conversation} ({self.kind})" + return f"{self.entity} - {self.channel} ({self.platform})" From f62cbda132441f55f62b542510d4ef3274934666 Mon Sep 17 00:00:00 2001 From: anurag2787 Date: Sat, 23 Aug 2025 16:39:40 +0530 Subject: [PATCH 6/6] updated the code according to the EntityChannel model --- .../slack/management/commands/owasp_match_channels.py | 11 ++++++----- backend/apps/slack/migrations/0019_entitychannel.py | 6 +++--- backend/apps/slack/models/entity_channel.py | 6 +++--- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/backend/apps/slack/management/commands/owasp_match_channels.py b/backend/apps/slack/management/commands/owasp_match_channels.py index ac121f4c79..c049296562 100644 --- a/backend/apps/slack/management/commands/owasp_match_channels.py +++ b/backend/apps/slack/management/commands/owasp_match_channels.py @@ -27,13 +27,14 @@ def handle(self, *args, **options): conversations = qs.filter(name__icontains=needle) for conv in conversations: _, was_created = EntityChannel.objects.get_or_create( - content_type=content_type, - object_id=entity.pk, - conversation=conv, + entity_id=entity.pk, + entity_type=content_type, + channel_id=conv.pk, + channel_type=ContentType.objects.get_for_model(Conversation), defaults={ - "is_main_channel": False, + "is_default": False, "is_reviewed": False, - "kind": "slack", + "platform": EntityChannel.Platform.SLACK, }, ) if was_created: diff --git a/backend/apps/slack/migrations/0019_entitychannel.py b/backend/apps/slack/migrations/0019_entitychannel.py index 97621bbbb6..49ae3a335c 100644 --- a/backend/apps/slack/migrations/0019_entitychannel.py +++ b/backend/apps/slack/migrations/0019_entitychannel.py @@ -20,8 +20,8 @@ class Migration(migrations.Migration): auto_created=True, primary_key=True, serialize=False, verbose_name="ID" ), ), - ("channel_id", models.PositiveIntegerField()), - ("entity_id", models.PositiveIntegerField()), + ("channel_id", models.PositiveBigIntegerField()), + ("entity_id", models.PositiveBigIntegerField()), ( "is_default", models.BooleanField( @@ -41,7 +41,7 @@ class Migration(migrations.Migration): choices=[("slack", "Slack")], default="slack", help_text="Platform of the channel (e.g., Slack)", - max_length=5, + max_length=32, ), ), ( diff --git a/backend/apps/slack/models/entity_channel.py b/backend/apps/slack/models/entity_channel.py index 4c83b8bae2..42bcd21d5b 100644 --- a/backend/apps/slack/models/entity_channel.py +++ b/backend/apps/slack/models/entity_channel.py @@ -23,7 +23,7 @@ class Meta: # Channel. channel = GenericForeignKey("channel_type", "channel_id") - channel_id = models.PositiveIntegerField() + channel_id = models.PositiveBigIntegerField() channel_type = models.ForeignKey( ContentType, on_delete=models.CASCADE, @@ -32,7 +32,7 @@ class Meta: # Entity. entity = GenericForeignKey("entity_type", "entity_id") - entity_id = models.PositiveIntegerField() + entity_id = models.PositiveBigIntegerField() entity_type = models.ForeignKey( ContentType, on_delete=models.CASCADE, @@ -48,7 +48,7 @@ class Meta: help_text="Indicates if the channel has been reviewed", ) platform = models.CharField( - max_length=5, + max_length=32, default=Platform.SLACK, choices=Platform.choices, help_text="Platform of the channel (e.g., Slack)",