diff --git a/backend/apps/owasp/admin/entity_channel.py b/backend/apps/owasp/admin/entity_channel.py index 2f2bcf3c86..1b04b4dd29 100644 --- a/backend/apps/owasp/admin/entity_channel.py +++ b/backend/apps/owasp/admin/entity_channel.py @@ -9,7 +9,7 @@ @admin.action(description="Mark selected EntityChannels as reviewed") def mark_as_reviewed(_modeladmin, request, queryset): - """Admin action to mark selected EntityChannels as reviewed.""" + """Mark selected EntityChannel records as reviewed.""" messages.success( request, f"Marked {queryset.update(is_reviewed=True)} EntityChannel(s) as reviewed.", @@ -62,7 +62,7 @@ class EntityChannelAdmin(admin.ModelAdmin): ) def channel_search_display(self, obj): - """Display the channel name for the selected channel.""" + """Return a readable channel label for admin display.""" if obj.channel_id and obj.channel_type: try: if obj.channel_type.model == "conversation": @@ -71,11 +71,10 @@ def channel_search_display(self, obj): except Conversation.DoesNotExist: return f"Channel {obj.channel_id} (not found)" return "-" - channel_search_display.short_description = "Channel Name" def get_form(self, request, obj=None, **kwargs): - """Get the form for the EntityChannel model.""" + """Return the admin form with Conversation content type metadata attached.""" 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..e49309a020 100644 --- a/backend/apps/owasp/admin/entity_member.py +++ b/backend/apps/owasp/admin/entity_member.py @@ -13,8 +13,8 @@ class EntityMemberAdmin(admin.ModelAdmin): - """Admin for EntityMember records (generic link to any OWASP entity).""" - + """Admin configuration for EntityMember records.""" + actions = ("approve_members",) autocomplete_fields = ("member",) list_display = ( @@ -42,7 +42,11 @@ class EntityMemberAdmin(admin.ModelAdmin): @admin.action(description="Approve selected members") def approve_members(self, request, queryset): - """Approve selected members.""" + """Admin action to approve selected entity members. + + Sets `is_active=True` and `is_reviewed=True` on all selected records + and displays a success message showing how many were updated. + """ self.message_user( request, f"Successfully approved {queryset.update(is_active=True, is_reviewed=True)} members.", @@ -50,7 +54,10 @@ def approve_members(self, request, queryset): @admin.display(description="Entity", ordering="entity_type") def entity(self, obj): - """Return entity link.""" + """Return a clickable admin link to the related entity. + + Example output: a link to the Project/Chapter/Committee admin change page. + """ return ( format_html( '{}', @@ -66,7 +73,7 @@ def entity(self, obj): @admin.display(description="OWASP URL", ordering="entity_type") def owasp_url(self, obj): - """Return entity OWASP site URL.""" + """Return a link to the OWASP site page of the linked entity.""" return ( format_html('↗️', obj.entity.owasp_url) if obj.entity @@ -74,7 +81,15 @@ def owasp_url(self, obj): ) def get_search_results(self, request, queryset, search_term): - """Get search results from entity name or key.""" + """Extend default search to also match entity names and keys. + + In addition to the built-in search, this method searches: + - Project name or key + - Chapter name or key + - Committee name or key + + and includes matching EntityMember rows in the results. + """ 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..c064bcf43f 100644 --- a/backend/apps/owasp/admin/member_profile.py +++ b/backend/apps/owasp/admin/member_profile.py @@ -72,7 +72,13 @@ class MemberProfileAdmin(admin.ModelAdmin): ) def get_queryset(self, request): - """Optimize queryset with select_related.""" + """ + Return an optimized queryset for the MemberProfile admin list view. + + This override applies `select_related("github_user")` to reduce the + number of SQL queries when displaying MemberProfile entries that include + related GitHub user information. + """ queryset = super().get_queryset(request) return queryset.select_related("github_user") diff --git a/backend/apps/owasp/admin/member_snapshot.py b/backend/apps/owasp/admin/member_snapshot.py index 84a7233852..8326575d60 100644 --- a/backend/apps/owasp/admin/member_snapshot.py +++ b/backend/apps/owasp/admin/member_snapshot.py @@ -108,7 +108,11 @@ class MemberSnapshotAdmin(admin.ModelAdmin): ) def get_queryset(self, request): - """Optimize queryset with select_related.""" + """Return an optimized queryset for the MemberSnapshot admin list view. + + Ensures related GitHub user information is loaded efficiently to + avoid unnecessary database queries in the admin list view. + """ queryset = super().get_queryset(request) return queryset.select_related("github_user") diff --git a/backend/apps/owasp/admin/mixins.py b/backend/apps/owasp/admin/mixins.py index 9addfc4a6e..8535019adc 100644 --- a/backend/apps/owasp/admin/mixins.py +++ b/backend/apps/owasp/admin/mixins.py @@ -12,7 +12,12 @@ class BaseOwaspAdminMixin: - """Base mixin for OWASP admin classes providing common patterns.""" + """Base mixin for OWASP admin classes. + + Provides common configuration patterns—such as default list_display, + list_filter, and search_fields—so individual ModelAdmin classes can avoid + duplicated boilerplate. + """ # Common configuration patterns. list_display_field_names = ( @@ -26,7 +31,7 @@ class BaseOwaspAdminMixin: ) def get_base_list_display(self, *additional_fields): - """Get base list display with additional fields.""" + """Construct a standard list_display value with optional extra fields.""" return tuple( ("name",) if hasattr(self.model, "name") else (), *additional_fields, @@ -34,12 +39,12 @@ def get_base_list_display(self, *additional_fields): ) def get_base_search_fields(self, *additional_fields): - """Get base search fields with additional fields.""" + """Construct a standard search_fields value with optional extra fields.""" return self.search_field_names + additional_fields class EntityMemberInline(GenericTabularInline): - """EntityMember inline for admin.""" + """Inline admin for EntityMember entries linking users to OWASP entities.""" ct_field = "entity_type" ct_fk_field = "entity_id" @@ -63,7 +68,7 @@ class EntityMemberInline(GenericTabularInline): class EntityChannelInline(GenericTabularInline): - """EntityChannel inline for admin.""" + """Inline admin interface for EntityChannel records associated with an entity.""" ct_field = "entity_type" ct_fk_field = "entity_id" @@ -80,7 +85,11 @@ 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 widgets for EntityChannel inline fields. + + - Uses a custom ChannelIdWidget for the channel_id field. + - Limits channel_type choices to only Slack Conversation content types. + """ if db_field.name == "channel_id": kwargs["widget"] = ChannelIdWidget() elif db_field.name == "channel_type": @@ -93,14 +102,26 @@ def formfield_for_dbfield(self, db_field, request, **kwargs): class GenericEntityAdminMixin(BaseOwaspAdminMixin): - """Mixin for generic entity admin with common entity functionality.""" + """Mixin providing common rendering logic for OWASP entity admin views. + + Adds helpers for displaying GitHub and OWASP links and prefetches related + repositories for performance. + """ def get_queryset(self, request): - """Get queryset with optimized relations.""" + """Extend the base queryset to prefetch related repositories. + + This reduces SQL queries when displaying GitHub-related fields. + """ return super().get_queryset(request).prefetch_related("repositories") def custom_field_github_urls(self, obj): - """Entity GitHub URLs with uniform formatting.""" + """Render GitHub URLs for the associated entity. + + Handles: + - Entities with multiple repositories (uses obj.repositories) + - Entities with a single owasp_repository field + """ if not hasattr(obj, "repositories"): if not hasattr(obj, "owasp_repository") or not obj.owasp_repository: return "" @@ -113,7 +134,7 @@ def custom_field_github_urls(self, obj): ) def custom_field_owasp_url(self, obj): - """Entity OWASP URL with uniform formatting.""" + """Render a link to the official OWASP entity webpage.""" if not hasattr(obj, "key") or not obj.key: return "" @@ -122,7 +143,7 @@ def custom_field_owasp_url(self, obj): ) def _format_github_link(self, repository): - """Format a single GitHub repository link.""" + """Format a GitHub repository link consistently.""" if not repository or not hasattr(repository, "owner") or not repository.owner: return "" if not hasattr(repository.owner, "login") or not repository.owner.login: @@ -140,12 +161,16 @@ def _format_github_link(self, repository): class StandardOwaspAdminMixin(BaseOwaspAdminMixin): - """Standard mixin for simple OWASP admin classes.""" + """Simple mixin for OWASP admin classes. + + Provides convenient helpers for generating common admin config + (list_display, list_filter, search_fields). + """ def get_common_config( self, extra_list_display=None, extra_search_fields=None, extra_list_filters=None ): - """Get common admin configuration to reduce boilerplate.""" + """Build a dictionary of common ModelAdmin configuration values.""" config = {} if extra_list_display: diff --git a/backend/apps/owasp/admin/project.py b/backend/apps/owasp/admin/project.py index ccbcd817fd..75343a37a3 100644 --- a/backend/apps/owasp/admin/project.py +++ b/backend/apps/owasp/admin/project.py @@ -54,7 +54,13 @@ class ProjectAdmin(admin.ModelAdmin, GenericEntityAdminMixin): ) def custom_field_name(self, obj) -> str: - """Project custom name.""" + """ + Return a display-friendly project name for the admin list view. + + If the project has a defined `name`, it is shown; otherwise the project + key is used as a fallback. This ensures that every project row has a + readable identifier even when optional fields are empty. + """ 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..e30c0a5b4e 100644 --- a/backend/apps/owasp/admin/project_health_metrics.py +++ b/backend/apps/owasp/admin/project_health_metrics.py @@ -29,7 +29,13 @@ class ProjectHealthMetricsAdmin(admin.ModelAdmin, StandardOwaspAdminMixin): search_fields = ("project__name",) def project(self, obj): - """Display project name.""" + """ + Return the name of the related project for display purposes. + + Used in the admin list view to show a readable project label instead + of the raw project foreign key reference. + + """ return obj.project.name if obj.project else "N/A" diff --git a/backend/apps/slack/admin/member.py b/backend/apps/slack/admin/member.py index 7dfa92e34a..61ba48b58f 100644 --- a/backend/apps/slack/admin/member.py +++ b/backend/apps/slack/admin/member.py @@ -24,7 +24,15 @@ class MemberAdmin(admin.ModelAdmin): ) def approve_suggested_users(self, request, queryset): - """Approve all suggested users for selected members, enforcing one-to-one constraints.""" + """ + Admin action to assign a suggested user to each selected Member. + + For each Member: + - If exactly one suggested user exists, it is assigned to the Member. + - If multiple suggested users exist, an error message is returned because only one can be assigned. + - If none exist, a warning message is shown. + + """ for entity in queryset: suggestions = entity.suggested_users.all()