From fe91cae207f341cc58e3c6bd24e806aff08f12d4 Mon Sep 17 00:00:00 2001 From: daveoconnor Date: Tue, 13 Jan 2026 13:31:55 -0800 Subject: [PATCH] Slack admin interface (#2057) --- slack/admin.py | 110 +++++++++++++++++++++++++++++++++++++++++++++++ slack/filters.py | 76 ++++++++++++++++++++++++++++++++ 2 files changed, 186 insertions(+) create mode 100644 slack/admin.py create mode 100644 slack/filters.py diff --git a/slack/admin.py b/slack/admin.py new file mode 100644 index 00000000..64286c49 --- /dev/null +++ b/slack/admin.py @@ -0,0 +1,110 @@ +from django.contrib import admin + +from slack.filters import FilterByReleaseDates +from slack.models import Channel, SlackActivityBucket, Thread + + +@admin.register(Channel) +class ChannelAdmin(admin.ModelAdmin): + list_display = ["id", "name", "last_update_ts_readable"] + search_fields = ["name", "id"] + readonly_fields = ["id", "name", "topic", "purpose", "last_update_ts"] + ordering = ["name"] + + @admin.display(description="Last Update") + def last_update_ts_readable(self, obj): + """Display last_update_ts in a human-readable format.""" + if obj.last_update_ts: + from slack.models import parse_ts + + return parse_ts(obj.last_update_ts).strftime("%Y-%m-%d %H:%M:%S UTC") + return "-" + + def has_add_permission(self, request): + return False + + def has_change_permission(self, request, obj=None): + return False + + def has_delete_permission(self, request, obj=None): + return False + + +@admin.register(SlackActivityBucket) +class SlackActivityBucketAdmin(admin.ModelAdmin): + list_display = ["day", "channel_name", "user_name", "count"] + search_fields = ["channel__name", "user__name", "user__real_name"] + list_filter = ["day", "channel__name", FilterByReleaseDates] + readonly_fields = ["day", "user", "channel", "count"] + raw_id_fields = ["user", "channel"] + date_hierarchy = "day" + ordering = ["-day"] + + @admin.display( + description="Channel", + ordering="channel__name", + ) + def channel_name(self, obj): + """Display channel name instead of Channel object.""" + return obj.channel.name if obj.channel else "-" + + @admin.display( + description="User", + ordering="user__name", + ) + def user_name(self, obj): + """Display user name instead of SlackUser object.""" + return obj.user.real_name or obj.user.name if obj.user else "-" + + def has_add_permission(self, request): + return False + + def has_change_permission(self, request, obj=None): + return False + + def has_delete_permission(self, request, obj=None): + return False + + +@admin.register(Thread) +class ThreadAdmin(admin.ModelAdmin): + list_display = [ + "id", + "channel__id", + "channel__name", + "thread_ts_readable", + "last_update_ts_readable", + ] + search_fields = ["channel__id", "channel__name", "thread_ts"] + list_filter = ["channel__name"] + readonly_fields = ["channel", "thread_ts", "last_update_ts", "db_created_at"] + raw_id_fields = ["channel"] + date_hierarchy = "db_created_at" + ordering = ["-db_created_at"] + + @admin.display(description="Thread Created") + def thread_ts_readable(self, obj): + """Display thread_ts in a human-readable format.""" + if obj.thread_ts: + from slack.models import parse_ts + + return parse_ts(obj.thread_ts).strftime("%Y-%m-%d %H:%M:%S UTC") + return "-" + + @admin.display(description="Last Update") + def last_update_ts_readable(self, obj): + """Display last_update_ts in a human-readable format.""" + if obj.last_update_ts: + from slack.models import parse_ts + + return parse_ts(obj.last_update_ts).strftime("%Y-%m-%d %H:%M:%S UTC") + return "-" + + def has_add_permission(self, request): + return False + + def has_change_permission(self, request, obj=None): + return False + + def has_delete_permission(self, request, obj=None): + return False diff --git a/slack/filters.py b/slack/filters.py new file mode 100644 index 00000000..55cd39bc --- /dev/null +++ b/slack/filters.py @@ -0,0 +1,76 @@ +from django.contrib import admin + + +class FilterByReleaseDates(admin.SimpleListFilter): + """Filter slack activity by Boost version release periods. + + The release date is considered the END of the development period for that version. + Messages are attributed to the version they were leading up to, not the one just released. + """ + + title = "release" + parameter_name = "release" + + def lookups(self, request, model_admin): + from versions.models import Version + + versions = Version.objects.filter( + release_date__isnull=False, active=True, full_release=True + ).order_by("-release_date")[:20] + + # Add special entries for ongoing development + choices = [ + ("master", "master (after latest release)"), + ("develop", "develop (after latest release)"), + ] + + # Add version entries + choices.extend([(v.id, f"{v.name} ({v.release_date})") for v in versions]) + + return choices + + def queryset(self, request, queryset): + if self.value() in ("master", "develop"): + # Get messages after the latest release + from versions.models import Version + + latest_version = ( + Version.objects.filter( + release_date__isnull=False, active=True, full_release=True + ) + .order_by("-release_date") + .first() + ) + + if latest_version and latest_version.release_date: + return queryset.filter(day__gt=latest_version.release_date) + + elif self.value(): + from versions.models import Version + + try: + version = Version.objects.get(id=self.value()) + if version.release_date: + # Get the previous version's release date (this is the start of the period) + previous_version = ( + Version.objects.filter( + release_date__lt=version.release_date, + active=True, + full_release=True, + ) + .order_by("-release_date") + .first() + ) + + if previous_version and previous_version.release_date: + # Filter messages between previous release and current release + return queryset.filter( + day__gt=previous_version.release_date, + day__lte=version.release_date, + ) + else: + # No previous version, so filter up to this release + return queryset.filter(day__lte=version.release_date) + except Version.DoesNotExist: + pass + return queryset