Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
7 changes: 3 additions & 4 deletions backend/apps/owasp/admin/entity_channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down Expand Up @@ -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":
Expand All @@ -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

Expand Down
27 changes: 21 additions & 6 deletions backend/apps/owasp/admin/entity_member.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
Expand Down Expand Up @@ -42,15 +42,22 @@ 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.",
)

@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(
'<a href="{}" target="_blank">{}</a>',
Expand All @@ -66,15 +73,23 @@ 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('<a href="{}" target="_blank">↗️</a>', 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."""
"""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:
Expand Down
8 changes: 7 additions & 1 deletion backend/apps/owasp/admin/member_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
6 changes: 5 additions & 1 deletion backend/apps/owasp/admin/member_snapshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
51 changes: 38 additions & 13 deletions backend/apps/owasp/admin/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
Expand All @@ -26,20 +31,20 @@ 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,
*self.list_display_field_names,
)
Comment on lines 35 to 39
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix get_base_list_display tuple construction (currently raises TypeError)

tuple() accepts at most one positional argument, but this call passes multiple arguments once additional_fields is non-empty:

return tuple(
    ("name",) if hasattr(self.model, "name") else (),
    *additional_fields,
    *self.list_display_field_names,
)

This will raise TypeError: tuple() takes at most 1 argument the first time get_base_list_display is called with extra fields. A safe implementation is to build the tuple via concatenation:

-        return tuple(
-            ("name",) if hasattr(self.model, "name") else (),
-            *additional_fields,
-            *self.list_display_field_names,
-        )
+        base = ("name",) if hasattr(self.model, "name") else ()
+        return base + additional_fields + self.list_display_field_names

This matches the docstring (“tuple[str]” combining name, custom fields, and timestamp fields) and avoids runtime errors.

🤖 Prompt for AI Agents
In backend/apps/owasp/admin/mixins.py around lines 45 to 49, the current use of
tuple(...) with multiple positional arguments causes TypeError because tuple()
accepts only one positional arg; change the construction to concatenate
sequences (for example, start with ("name",) if model has name else (), then +
additional_fields + tuple(self.list_display_field_names)) or build a list and
call tuple(...) once so the result is a single tuple[str] combining the name
field (when present), any additional_fields, and the list_display_field_names.


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"
Expand All @@ -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"
Expand All @@ -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":
Expand All @@ -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 ""
Expand All @@ -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 ""

Expand All @@ -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:
Expand All @@ -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:
Expand Down
8 changes: 7 additions & 1 deletion backend/apps/owasp/admin/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
8 changes: 7 additions & 1 deletion backend/apps/owasp/admin/project_health_metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"


Expand Down
10 changes: 9 additions & 1 deletion backend/apps/slack/admin/member.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down