diff --git a/backend/apps/owasp/admin/entity_channel.py b/backend/apps/owasp/admin/entity_channel.py
index 2f2bcf3c86..bb4d263fce 100644
--- a/backend/apps/owasp/admin/entity_channel.py
+++ b/backend/apps/owasp/admin/entity_channel.py
@@ -1,5 +1,6 @@
"""Admin configuration for the EntityChannel model."""
+from django import forms
from django.contrib import admin, messages
from django.contrib.contenttypes.models import ContentType
@@ -8,8 +9,15 @@
@admin.action(description="Mark selected EntityChannels as reviewed")
-def mark_as_reviewed(_modeladmin, request, queryset):
- """Admin action to mark selected EntityChannels as reviewed."""
+def mark_as_reviewed(_modeladmin, request, queryset) -> None:
+ """Admin action to mark selected EntityChannels as reviewed.
+
+ Args:
+ _modeladmin: The ModelAdmin instance (unused).
+ request: The HTTP request object.
+ queryset: QuerySet of EntityChannel instances to mark as reviewed.
+
+ """
messages.success(
request,
f"Marked {queryset.update(is_reviewed=True)} EntityChannel(s) as reviewed.",
@@ -61,8 +69,19 @@ class EntityChannelAdmin(admin.ModelAdmin):
"channel_id",
)
- def channel_search_display(self, obj):
- """Display the channel name for the selected channel."""
+ def channel_search_display(self, obj) -> str:
+ """Display the channel name in the admin list view.
+
+ Retrieves and displays the Conversation name if the channel_id
+ references a valid Slack conversation.
+
+ Args:
+ obj: The EntityChannel instance.
+
+ Returns:
+ str: Channel name as '#name', error message, or '-' if not found.
+
+ """
if obj.channel_id and obj.channel_type:
try:
if obj.channel_type.model == "conversation":
@@ -74,8 +93,21 @@ def channel_search_display(self, obj):
channel_search_display.short_description = "Channel Name"
- def get_form(self, request, obj=None, **kwargs):
- """Get the form for the EntityChannel model."""
+ def get_form(self, request, obj=None, **kwargs) -> forms.ModelForm:
+ """Get the form for the EntityChannel model.
+
+ Prepares the form with necessary metadata for channel selection
+ and custom widgets.
+
+ Args:
+ request: The HTTP request object.
+ obj: The EntityChannel instance being edited (None for add).
+ **kwargs: Additional keyword arguments passed to parent.
+
+ Returns:
+ Form: The EntityChannel form with conversation content type metadata.
+
+ """
form = super().get_form(request, obj, **kwargs)
form.conversation_content_type_id = ContentType.objects.get_for_model(Conversation).id
diff --git a/backend/apps/owasp/admin/entity_member.py b/backend/apps/owasp/admin/entity_member.py
index 1aef460fbb..1290b2c014 100644
--- a/backend/apps/owasp/admin/entity_member.py
+++ b/backend/apps/owasp/admin/entity_member.py
@@ -2,6 +2,7 @@
from django.contrib import admin
from django.contrib.contenttypes.models import ContentType
+from django.db import models
from django.db.models import Q
from django.urls import reverse
from django.utils.html import format_html
@@ -41,16 +42,32 @@ class EntityMemberAdmin(admin.ModelAdmin):
ordering = ("member__name", "order")
@admin.action(description="Approve selected members")
- def approve_members(self, request, queryset):
- """Approve selected members."""
+ def approve_members(self, request, queryset) -> None:
+ """Admin action to approve selected members.
+
+ Sets is_active and is_reviewed flags for selected entity members.
+
+ Args:
+ request: The HTTP request object.
+ queryset: QuerySet of EntityMember instances to approve.
+
+ """
self.message_user(
request,
f"Successfully approved {queryset.update(is_active=True, is_reviewed=True)} members.",
)
@admin.display(description="Entity", ordering="entity_type")
- def entity(self, obj):
- """Return entity link."""
+ def entity(self, obj) -> str:
+ """Display entity as a link in the admin list view.
+
+ Args:
+ obj: The EntityMember instance.
+
+ Returns:
+ str: HTML link to the entity's admin page or '-' if entity is missing.
+
+ """
return (
format_html(
'{}',
@@ -65,16 +82,39 @@ def entity(self, obj):
)
@admin.display(description="OWASP URL", ordering="entity_type")
- def owasp_url(self, obj):
- """Return entity OWASP site URL."""
+ def owasp_url(self, obj) -> str:
+ """Display entity OWASP website link in admin list view.
+
+ Args:
+ obj: The EntityMember instance.
+
+ Returns:
+ str: HTML link to the entity's OWASP website or '-' if entity is missing.
+
+ """
return (
format_html('↗️', obj.entity.owasp_url)
if obj.entity
else "-"
)
- def get_search_results(self, request, queryset, search_term):
- """Get search results from entity name or key."""
+ def get_search_results(self, request, queryset, search_term) -> tuple[models.QuerySet, bool]:
+ """Extend search results to include entity name or key matches.
+
+ Searches across Project, Chapter, and Committee entities by name or key
+ and includes matching results in the EntityMember search results.
+
+ Args:
+ request: The HTTP request object.
+ queryset: Initial QuerySet of EntityMember instances.
+ search_term: The search term entered by the user.
+
+ Returns:
+ tuple: (QuerySet, use_distinct) where QuerySet includes matching members
+ and related entities by name/key, and use_distinct indicates
+ if DISTINCT should be applied to the query.
+
+ """
queryset, use_distinct = super().get_search_results(request, queryset, search_term)
if search_term:
diff --git a/backend/apps/owasp/admin/member_profile.py b/backend/apps/owasp/admin/member_profile.py
index 9e5ec7ddaa..412bc9eb38 100644
--- a/backend/apps/owasp/admin/member_profile.py
+++ b/backend/apps/owasp/admin/member_profile.py
@@ -1,6 +1,7 @@
"""Django admin configuration for MemberProfile model."""
from django.contrib import admin
+from django.db import models
from apps.owasp.models.member_profile import MemberProfile
@@ -71,10 +72,20 @@ class MemberProfileAdmin(admin.ModelAdmin):
),
)
- def get_queryset(self, request):
- """Optimize queryset with select_related."""
- queryset = super().get_queryset(request)
- return queryset.select_related("github_user")
+ def get_queryset(self, request) -> models.QuerySet:
+ """Retrieve optimized queryset with related GitHub user.
+
+ Applies select_related on github_user to reduce database queries when displaying
+ member profile lists.
+
+ Args:
+ request: The HTTP request object.
+
+ Returns:
+ QuerySet: MemberProfile queryset with prefetched github_user relations.
+
+ """
+ return super().get_queryset(request).select_related("github_user")
admin.site.register(MemberProfile, MemberProfileAdmin)
diff --git a/backend/apps/owasp/admin/member_snapshot.py b/backend/apps/owasp/admin/member_snapshot.py
index 84a7233852..aa764c51cd 100644
--- a/backend/apps/owasp/admin/member_snapshot.py
+++ b/backend/apps/owasp/admin/member_snapshot.py
@@ -1,6 +1,7 @@
"""Django admin configuration for MemberSnapshot model."""
from django.contrib import admin
+from django.db import models
from apps.owasp.models.member_snapshot import MemberSnapshot
@@ -107,10 +108,20 @@ class MemberSnapshotAdmin(admin.ModelAdmin):
),
)
- def get_queryset(self, request):
- """Optimize queryset with select_related."""
- queryset = super().get_queryset(request)
- return queryset.select_related("github_user")
+ def get_queryset(self, request) -> models.QuerySet:
+ """Retrieve optimized queryset with related GitHub user.
+
+ Applies select_related on github_user to reduce database queries when displaying
+ member snapshot lists.
+
+ Args:
+ request: The HTTP request object.
+
+ Returns:
+ QuerySet: MemberSnapshot queryset with select_related("github_user") applied.
+
+ """
+ return super().get_queryset(request).select_related("github_user")
admin.site.register(MemberSnapshot, MemberSnapshotAdmin)
diff --git a/backend/apps/owasp/admin/mixins.py b/backend/apps/owasp/admin/mixins.py
index 59b7839484..96137e9042 100644
--- a/backend/apps/owasp/admin/mixins.py
+++ b/backend/apps/owasp/admin/mixins.py
@@ -2,6 +2,7 @@
from django.contrib.contenttypes.admin import GenericTabularInline
from django.contrib.contenttypes.models import ContentType
+from django.db import models
from django.utils.html import format_html
from django.utils.safestring import mark_safe
@@ -25,16 +26,29 @@ class BaseOwaspAdminMixin:
"key",
)
- def get_base_list_display(self, *additional_fields):
- """Get base list display with additional fields."""
- return tuple(
- ("name",) if hasattr(self.model, "name") else (),
- *additional_fields,
- *self.list_display_field_names,
- )
+ def get_base_list_display(self, *additional_fields) -> tuple[str, ...]:
+ """Build base list display tuple with additional fields.
+
+ Args:
+ *additional_fields: Extra field names to include in list display.
+
+ Returns:
+ tuple: Base display fields (name, timestamps) plus additional fields.
+
+ """
+ base_fields = ("name",) if hasattr(self.model, "name") else ()
+ return base_fields + tuple(additional_fields) + self.list_display_field_names
+
+ def get_base_search_fields(self, *additional_fields) -> tuple[str, ...]:
+ """Build base search fields tuple with additional fields.
+
+ Args:
+ *additional_fields: Extra field names to include in search.
+
+ Returns:
+ tuple: Base search fields (name, key) plus additional fields.
- def get_base_search_fields(self, *additional_fields):
- """Get base search fields with additional fields."""
+ """
return self.search_field_names + additional_fields
@@ -80,7 +94,20 @@ class EntityChannelInline(GenericTabularInline):
ordering = ("platform", "channel_id")
def formfield_for_dbfield(self, db_field, request, **kwargs):
- """Override to add custom widget for channel_id field and limit channel_type options."""
+ """Customize form fields for EntityChannel inline editing.
+
+ Adds a custom widget for channel_id field and limits channel_type options
+ to only Conversation (Slack channels).
+
+ Args:
+ db_field: The database field being customized.
+ request: The HTTP request object.
+ **kwargs: Additional keyword arguments passed to parent.
+
+ Returns:
+ Field: The customized form field.
+
+ """
if db_field.name == "channel_id":
kwargs["widget"] = ChannelIdWidget()
elif db_field.name == "channel_type":
@@ -95,12 +122,28 @@ def formfield_for_dbfield(self, db_field, request, **kwargs):
class GenericEntityAdminMixin(BaseOwaspAdminMixin):
"""Mixin for generic entity admin with common entity functionality."""
- def get_queryset(self, request):
- """Get queryset with optimized relations."""
- return super().get_queryset(request).prefetch_related("repositories")
+ def get_queryset(self, request) -> models.QuerySet:
+ """Retrieve optimized queryset with prefetched repositories.
+
+ Args:
+ request: The HTTP request object.
+
+ Returns:
+ QuerySet: Queryset with prefetched repositories for efficient display.
+
+ """
+ return super().get_queryset(request).prefetch_related("repositories") # type: ignore[misc]
- def custom_field_github_urls(self, obj):
- """Entity GitHub URLs with uniform formatting."""
+ def custom_field_github_urls(self, obj) -> str:
+ """Display entity GitHub repository links in admin list view.
+
+ Args:
+ obj: The entity object with repositories.
+
+ Returns:
+ str: HTML-formatted links to GitHub repositories.
+
+ """
if not hasattr(obj, "repositories"):
if not hasattr(obj, "owasp_repository") or not obj.owasp_repository:
return ""
@@ -112,15 +155,31 @@ def custom_field_github_urls(self, obj):
" ".join(links)
)
- def custom_field_owasp_url(self, obj):
- """Entity OWASP URL with uniform formatting."""
+ def custom_field_owasp_url(self, obj) -> str:
+ """Display entity OWASP website link in admin list view.
+
+ Args:
+ obj: The entity object with a key attribute.
+
+ Returns:
+ str: HTML-formatted link to OWASP website or empty string.
+
+ """
if not hasattr(obj, "key") or not obj.key:
return ""
return format_html("↗️", obj.key)
- def _format_github_link(self, repository):
- """Format a single GitHub repository link."""
+ def _format_github_link(self, repository) -> str:
+ """Format a GitHub repository link as HTML.
+
+ Args:
+ repository: The repository object with owner and key attributes.
+
+ Returns:
+ str: HTML-formatted link to GitHub repository or empty string if invalid.
+
+ """
if not repository or not hasattr(repository, "owner") or not repository.owner:
return ""
if not hasattr(repository.owner, "login") or not repository.owner.login:
@@ -143,8 +202,20 @@ class StandardOwaspAdminMixin(BaseOwaspAdminMixin):
def get_common_config(
self, extra_list_display=None, extra_search_fields=None, extra_list_filters=None
- ):
- """Get common admin configuration to reduce boilerplate."""
+ ) -> dict:
+ """Build common admin configuration dictionary.
+
+ Reduces boilerplate by merging base fields with custom additions.
+
+ Args:
+ extra_list_display: Additional fields to display in list view.
+ extra_search_fields: Additional fields to include in search.
+ extra_list_filters: Additional fields to use for filtering.
+
+ Returns:
+ dict: Configuration dictionary with list_display, search_fields, and list_filter.
+
+ """
config = {}
if extra_list_display:
diff --git a/backend/apps/owasp/admin/project.py b/backend/apps/owasp/admin/project.py
index ccbcd817fd..28d7e7ae35 100644
--- a/backend/apps/owasp/admin/project.py
+++ b/backend/apps/owasp/admin/project.py
@@ -54,7 +54,18 @@ class ProjectAdmin(admin.ModelAdmin, GenericEntityAdminMixin):
)
def custom_field_name(self, obj) -> str:
- """Project custom name."""
+ """Display project name or key in admin list view.
+
+ Uses the project name as the primary display, falling back to the key
+ if the name is not available.
+
+ Args:
+ obj: The Project instance.
+
+ Returns:
+ str: The project name or key.
+
+ """
return f"{obj.name or obj.key}"
custom_field_name.short_description = "Name"
diff --git a/backend/apps/owasp/admin/project_health_metrics.py b/backend/apps/owasp/admin/project_health_metrics.py
index bcc9574d6b..16fd662389 100644
--- a/backend/apps/owasp/admin/project_health_metrics.py
+++ b/backend/apps/owasp/admin/project_health_metrics.py
@@ -28,8 +28,16 @@ class ProjectHealthMetricsAdmin(admin.ModelAdmin, StandardOwaspAdminMixin):
)
search_fields = ("project__name",)
- def project(self, obj):
- """Display project name."""
+ def project(self, obj) -> str:
+ """Display project name in admin list view.
+
+ Args:
+ obj: The ProjectHealthMetrics instance.
+
+ Returns:
+ str: The project name or 'N/A' if no project is associated.
+
+ """
return obj.project.name if obj.project else "N/A"
diff --git a/backend/apps/owasp/admin/widgets.py b/backend/apps/owasp/admin/widgets.py
index 409fdee692..bf56e80934 100644
--- a/backend/apps/owasp/admin/widgets.py
+++ b/backend/apps/owasp/admin/widgets.py
@@ -7,13 +7,35 @@
class ChannelIdWidget(forms.TextInput):
"""Custom widget for channel_id with search functionality."""
- def __init__(self, *args, **kwargs):
- """Initialize the widget with custom attributes."""
+ def __init__(self, *args, **kwargs) -> None:
+ """Initialize the ChannelIdWidget.
+
+ Sets up custom CSS classes and placeholder text for the text input field.
+
+ Args:
+ *args: Positional arguments passed to parent TextInput.
+ **kwargs: Keyword arguments passed to parent TextInput.
+
+ """
super().__init__(*args, **kwargs)
self.attrs.update({"class": "vForeignKeyRawIdAdminField", "placeholder": "Channel ID"})
- def render(self, name, value, attrs=None, renderer=None):
- """Render the widget with a search button."""
+ def render(self, name, value, attrs=None, renderer=None) -> str:
+ """Render the widget with a search button.
+
+ Displays the text input field alongside a lookup button for selecting
+ related Slack conversation objects.
+
+ Args:
+ name: The HTML field name.
+ value: The current field value.
+ attrs: Optional HTML attributes for the widget.
+ renderer: Optional form renderer instance.
+
+ Returns:
+ str: HTML markup for the widget and search button.
+
+ """
widget_html = super().render(name, value, attrs, renderer)
search_button = (
diff --git a/backend/apps/slack/admin/member.py b/backend/apps/slack/admin/member.py
index 7dfa92e34a..e658ae2750 100644
--- a/backend/apps/slack/admin/member.py
+++ b/backend/apps/slack/admin/member.py
@@ -23,8 +23,21 @@ class MemberAdmin(admin.ModelAdmin):
"user__login",
)
- def approve_suggested_users(self, request, queryset):
- """Approve all suggested users for selected members, enforcing one-to-one constraints."""
+ def approve_suggested_users(self, request, queryset) -> None:
+ """Approve suggested users for selected Slack members.
+
+ For each member in the selection, if exactly one suggested user exists,
+ assign that user to the member. If multiple or no suggested users exist,
+ display appropriate warning or error messages.
+
+ Args:
+ request: The HTTP request object.
+ queryset: The queryset of Member instances to process.
+
+ Returns:
+ None: Displays messages to the user via Django's messages framework.
+
+ """
for entity in queryset:
suggestions = entity.suggested_users.all()