Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/apps/slack/admin/__init__.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
23 changes: 23 additions & 0 deletions backend/apps/slack/admin/entity_channel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""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
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")
actions = [mark_as_reviewed]
50 changes: 50 additions & 0 deletions backend/apps/slack/management/commands/owasp_match_channels.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"""Management 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
from apps.owasp.models.committee import Committee
from apps.owasp.models.project import Project
from apps.slack.models import Conversation, EntityChannel


class Command(BaseCommand):
help = "Populate EntityChannel links for Chapters, Committees, and Projects."

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 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."))
48 changes: 48 additions & 0 deletions backend/apps/slack/migrations/0019_entitychannel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Generated by Django for EntityChannel model

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"),
]

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",
},
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# 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",
),
]
1 change: 1 addition & 0 deletions backend/apps/slack/models/__init__.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
28 changes: 28 additions & 0 deletions backend/apps/slack/models/entity_channel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""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")
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 a readable string representation of the EntityChannel."""
return f"{self.entity} - {self.conversation} ({self.kind})"