From 5616ce8a4b6843facc9d8e15ca234e9069f07a7e Mon Sep 17 00:00:00 2001 From: Moiz Zulfiqar Date: Sun, 25 Jan 2026 05:36:54 +0500 Subject: [PATCH 1/5] Fix docstring formatting per ruff checks --- .../apps/github/models/mixins/repository.py | 138 ++++++++++++-- backend/apps/github/models/mixins/user.py | 170 ++++++++++++++--- backend/apps/owasp/models/project.py | 175 +++++++++++++++--- 3 files changed, 416 insertions(+), 67 deletions(-) diff --git a/backend/apps/github/models/mixins/repository.py b/backend/apps/github/models/mixins/repository.py index 6bd7b27b6c..63767cd05a 100644 --- a/backend/apps/github/models/mixins/repository.py +++ b/backend/apps/github/models/mixins/repository.py @@ -14,7 +14,15 @@ class RepositoryIndexMixin: @property def is_indexable(self) -> bool: - """Repositories to index.""" + """Determine if the repository should be indexed. + + Returns True if the repository is not archived, not empty, not a template, + and is associated with at least one project. + + Returns: + bool: True if repository meets all indexing criteria, False otherwise. + + """ return ( not self.is_archived and not self.is_empty @@ -24,90 +32,182 @@ def is_indexable(self) -> bool: @property def idx_commits_count(self) -> int: - """Return commits count for indexing.""" + """Get the total number of commits in this repository for indexing. + + Returns: + int: The total commit count of the repository. + + """ return self.commits_count @property def idx_contributors_count(self) -> int: - """Return contributors count for indexing.""" + """Get the total number of contributors to this repository for indexing. + + Returns: + int: The total number of unique contributors. + + """ return self.contributors_count @property def idx_created_at(self) -> float: - """Return created at for indexing.""" + """Get the repository creation timestamp for indexing. + + Returns: + float: Unix timestamp (seconds since epoch) when the repository was created. + + """ return self.created_at.timestamp() @property def idx_description(self) -> str: - """Return description for indexing.""" + """Get the repository description for indexing. + + Returns: + str: The repository's description text. + + """ return self.description @property def idx_forks_count(self) -> int: - """Return forks count for indexing.""" + """Get the total number of repository forks for indexing. + + Returns: + int: The total number of times this repository has been forked. + + """ return self.forks_count @property def idx_has_funding_yml(self) -> bool: - """Return has funding.yml for indexing.""" + """Check if the repository has a FUNDING.yml file for indexing. + + Returns: + bool: True if the repository contains a FUNDING.yml file, False otherwise. + + """ return self.has_funding_yml @property def idx_key(self) -> str: - """Return key for indexing.""" + """Get the unique Nest key identifier for this repository for indexing. + + Returns: + str: The repository's unique Nest key. + + """ return self.nest_key @property def idx_languages(self) -> list[str]: - """Return languages for indexing.""" + """Get the programming languages used in this repository for indexing. + + Returns: + list[str]: A list of programming language names detected in the repository. + + """ return self.languages @property def idx_license(self) -> str: - """Return license for indexing.""" + """Get the repository's license identifier for indexing. + + Returns: + str: The SPDX license identifier or name, if available. + + """ return self.license @property def idx_name(self) -> str: - """Return name for indexing.""" + """Get the repository name for indexing. + + Returns: + str: The name of the repository. + + """ return self.name @property def idx_open_issues_count(self) -> int: - """Return open issues count for indexing.""" + """Get the total number of open issues in this repository for indexing. + + Returns: + int: The count of currently open issues and pull requests. + + """ return self.open_issues_count @property def idx_project_key(self) -> str: - """Return project key for indexing.""" + """Get the Nest key of the associated project for indexing. + + Returns: + str: The unique Nest key of the project this repository belongs to, + or an empty string if no project is associated. + + """ return self.project.nest_key if self.project else "" @property def idx_pushed_at(self) -> float: - """Return pushed at for indexing.""" + """Get the timestamp of the last push to this repository for indexing. + + Returns: + float: Unix timestamp (seconds since epoch) of the most recent push. + + """ return self.pushed_at.timestamp() @property def idx_size(self) -> int: - """Return size for indexing.""" + """Get the repository size in kilobytes for indexing. + + Returns: + int: The repository size in KB. + + """ return self.size @property def idx_stars_count(self) -> int: - """Return stars count for indexing.""" + """Get the total number of stars (likes) this repository has received for indexing. + + Returns: + int: The total count of stars/favorites on the repository. + + """ return self.stars_count @property def idx_subscribers_count(self) -> int: - """Return subscribers count for indexing.""" + """Get the total number of watchers/subscribers for this repository for indexing. + + Returns: + int: The count of users watching this repository. + + """ return self.stars_count @property def idx_top_contributors(self) -> list[dict[str, Any]]: - """Return top contributors for indexing.""" + """Get the list of top contributors to this repository for indexing. + + Returns: + list[dict[str, Any]]: A list of dictionaries containing information about + the top contributors to this repository. + + """ return RepositoryContributor.get_top_contributors(repository=self.key) @property def idx_topics(self): - """Return topics for indexing.""" + """Get the topics/tags associated with this repository for indexing. + + Returns: + list: A list of topic tags that categorize this repository. + + """ return self.topics diff --git a/backend/apps/github/models/mixins/user.py b/backend/apps/github/models/mixins/user.py index bdbb2c2f19..a8f1902744 100644 --- a/backend/apps/github/models/mixins/user.py +++ b/backend/apps/github/models/mixins/user.py @@ -12,7 +12,16 @@ class UserIndexMixin: @property def is_indexable(self): - """Users to index.""" + """Determine if the user should be indexed. + + A user is considered indexable if they are not a bot, their login does not + end with common bot suffixes ("Bot" or "-bot"), and their login is not + present in the non-indexable logins list. + + Returns: + bool: True if the user meets all indexing criteria, False otherwise. + + """ return ( not self.is_bot and not self.login.endswith(("Bot", "-bot")) @@ -21,77 +30,154 @@ def is_indexable(self): @property def idx_avatar_url(self) -> str: - """Return avatar URL for indexing.""" + """Get the user's avatar URL for indexing. + + Returns: + str: The URL of the user's avatar image. + + """ return self.avatar_url @property def idx_badge_count(self) -> int: - """Return badge count for indexing.""" + """Get the number of active badges associated with the user. + + Returns: + int: The count of active user badges. + + """ return self.user_badges.filter(is_active=True).count() @property def idx_bio(self) -> str: - """Return bio for indexing.""" + """Get the user's biography text for indexing. + + Returns: + str: The user's bio, if provided; otherwise an empty string. + + """ return self.bio @property def idx_company(self) -> str: - """Return company for indexing.""" + """Get the user's company affiliation for indexing. + + Returns: + str: The company name associated with the user's profile, if any. + + """ return self.company @property def idx_created_at(self) -> float: - """Return created at timestamp for indexing.""" + """Get the account creation timestamp for indexing. + + Returns: + float: Unix timestamp (seconds since epoch) when the account was created. + + """ return self.created_at.timestamp() @property def idx_email(self) -> str: - """Return email for indexing.""" + """Get the user's email address for indexing. + + Returns: + str: The user's email address, if available. + + """ return self.email @property def idx_key(self) -> str: - """Return key for indexing.""" + """Get the unique key identifier for this user used in indexing. + + Returns: + str: The user's login, used as the unique key. + + """ return self.login @property def idx_followers_count(self) -> int: - """Return followers count for indexing.""" + """Get the number of followers for the user. + + Returns: + int: Total count of users following this user. + + """ return self.followers_count @property def idx_following_count(self) -> int: - """Return following count for indexing.""" + """Get the number of users the user is following. + + Returns: + int: Total count of users this user follows. + + """ return self.following_count @property def idx_location(self) -> str: - """Return location for indexing.""" + """Get the user's location for indexing. + + Returns: + str: The user's self-declared location, if available. + + """ return self.location @property def idx_login(self) -> str: - """Return login for indexing.""" + """Get the user's login for indexing. + + Returns: + str: The user's GitHub login handle. + + """ return self.login @property def idx_name(self) -> str: - """Return name for indexing.""" + """Get the user's display name for indexing. + + Returns: + str: The full name displayed on the user's profile, if provided. + + """ return self.name @property def idx_public_repositories_count(self) -> int: - """Return public repositories count for indexing.""" + """Get the number of public repositories owned by the user. + + Returns: + int: Total count of public repositories owned by the user. + + """ return self.public_repositories_count @property def idx_title(self) -> str: - """Return title for indexing.""" + """Get the user's profile title or headline for indexing. + + Returns: + str: The title or headline associated with the user, if any. + + """ return self.title @property def idx_contributions(self): - """Return contributions for indexing.""" + """Get a summary of the user's top repository contributions for indexing. + + Returns: + list[dict]: A list of contribution summaries, each including counts and + metadata about the contributed repositories (e.g., name, stars, + forks, license, and latest release information). + + """ from apps.github.models.repository_contributor import RepositoryContributor return [ @@ -123,12 +209,23 @@ def idx_contributions(self): @property def idx_contributions_count(self) -> int: - """Return contributions count for indexing.""" + """Get the total number of contributions made by the user. + + Returns: + int: Aggregate contributions count across repositories. + + """ return self.contributions_count @property def idx_issues(self) -> list[dict]: - """Return issues for indexing.""" + """Get recent issues associated with the user for indexing. + + Returns: + list[dict]: A list of issue summaries including timestamps, counts, + identifiers, titles, URLs, and minimal repository metadata. + + """ return [ { "created_at": i.created_at.timestamp(), @@ -149,12 +246,24 @@ def idx_issues(self) -> list[dict]: @property def idx_issues_count(self) -> int: - """Return issues count for indexing.""" + """Get the total number of issues associated with the user. + + Returns: + int: Count of issues linked to the user. + + """ return self.issues.count() @property def idx_releases(self) -> list[dict]: - """Return releases for indexing.""" + """Get recent releases associated with the user for indexing. + + Returns: + list[dict]: A list of release summaries including pre-release flag, + names, published timestamps, tags, URLs, and minimal repository + metadata. + + """ return [ { "is_pre_release": r.is_pre_release, @@ -175,15 +284,30 @@ def idx_releases(self) -> list[dict]: @property def idx_releases_count(self) -> int: - """Return releases count for indexing.""" + """Get the total number of releases associated with the user. + + Returns: + int: Count of releases linked to the user. + + """ return self.releases.count() @property def idx_updated_at(self) -> float: - """Return updated at timestamp for indexing.""" + """Get the last profile update timestamp for indexing. + + Returns: + float: Unix timestamp (seconds since epoch) of the last update. + + """ return self.updated_at.timestamp() @property def idx_url(self) -> str: - """Return GitHub profile URL for indexing.""" + """Get the user's GitHub profile URL for indexing. + + Returns: + str: The URL to the user's GitHub profile page. + + """ return self.url diff --git a/backend/apps/owasp/models/project.py b/backend/apps/owasp/models/project.py index 0a6ecdcafb..5d65eb0c8b 100644 --- a/backend/apps/owasp/models/project.py +++ b/backend/apps/owasp/models/project.py @@ -159,44 +159,84 @@ def __str__(self) -> str: @property def entity_leaders(self) -> list[EntityMember]: - """Return project leaders.""" + """Get the list of project leaders. + + Returns: + list[EntityMember]: Up to the top leaders limited by `MAX_LEADERS_COUNT`. + + """ return super().entity_leaders[:MAX_LEADERS_COUNT] @property def health_score(self) -> float | None: - """Return project health score.""" + """Get the latest computed health score for the project. + + Returns: + float | None: The most recent health score, or None if unavailable. + + """ return self.last_health_metrics.score if self.last_health_metrics else None @property def is_code_type(self) -> bool: - """Indicate whether project has CODE type.""" + """Check if the project's type is CODE. + + Returns: + bool: True if the project type equals `ProjectType.CODE`. + + """ return self.type == ProjectType.CODE @property def is_documentation_type(self) -> bool: - """Indicate whether project has DOCUMENTATION type.""" + """Check if the project's type is DOCUMENTATION. + + Returns: + bool: True if the project type equals `ProjectType.DOCUMENTATION`. + + """ return self.type == ProjectType.DOCUMENTATION @property def is_funding_requirements_compliant(self) -> bool: - """Indicate whether project is compliant with funding requirements.""" + """Check if the project complies with funding requirements. + + Returns: + bool: True if all related repositories are funding-policy compliant. + + """ return not self.repositories.filter(is_funding_policy_compliant=False).exists() @property def is_leader_requirements_compliant(self) -> bool: - """Indicate whether project is compliant with project leaders requirements.""" + """Check if the project satisfies OWASP leader requirements. + + Returns: + bool: True if the project has multiple leaders (more than one). + + """ # https://owasp.org/www-committee-project/#div-practice # Have multiple Project Leaders who are not all employed by the same company. return self.leaders_count > 1 @property def is_tool_type(self) -> bool: - """Indicate whether project has TOOL type.""" + """Check if the project's type is TOOL. + + Returns: + bool: True if the project type equals `ProjectType.TOOL`. + + """ return self.type == ProjectType.TOOL @property def issues(self): - """Return issues.""" + """Get all issues across the project's repositories. + + Returns: + QuerySet[Issue]: A queryset of issues with related entities prefetched. + + """ return ( Issue.objects.filter( repository__in=self.repositories.all(), @@ -215,32 +255,62 @@ def issues(self): @property def issues_count(self) -> int: - """Return count of issues.""" + """Get the total number of issues. + + Returns: + int: Count of issues across the project's repositories. + + """ return self.issues.count() @property def last_health_metrics(self) -> ProjectHealthMetrics | None: - """Return last health metrics for the project.""" + """Get the most recent health metrics for the project. + + Returns: + ProjectHealthMetrics | None: Latest metrics record or None if missing. + + """ return self.health_metrics.order_by("-nest_created_at").first() @property def leaders_count(self) -> int: - """Return the count of leaders.""" + """Get the number of project leaders. + + Returns: + int: Count of leaders derived from `leaders_raw`. + + """ return len(self.leaders_raw) @property def nest_key(self) -> str: - """Get Nest key.""" + """Get the Nest-specific project key. + + Returns: + str: The project key with the "www-project-" prefix removed. + + """ return self.key.replace("www-project-", "") @property def nest_url(self) -> str: - """Get Nest URL for project.""" + """Get the absolute Nest URL for this project. + + Returns: + str: The full Nest URL pointing to the project's page. + + """ return get_absolute_url(f"/projects/{self.nest_key}") @property def open_issues(self): - """Return open issues.""" + """Get all open issues across the project's repositories. + + Returns: + QuerySet[Issue]: A queryset of open issues with repository related data. + + """ return Issue.open_issues.filter( repository__in=self.repositories.all(), ).select_related( @@ -249,17 +319,32 @@ def open_issues(self): @property def open_pull_requests_count(self) -> int: - """Return count of open pull requests.""" + """Get the number of open pull requests. + + Returns: + int: Count of pull requests currently in the "open" state. + + """ return self.pull_requests.filter(state="open").count() @property def owasp_page_last_updated_at(self) -> datetime.datetime | None: - """Return the last updated date of the OWASP page.""" + """Get the last updated timestamp of the project's OWASP page. + + Returns: + datetime.datetime | None: The OWASP page's last update time, or None. + + """ return self.owasp_repository.updated_at if self.owasp_repository else None @property def pull_requests(self): - """Return pull requests.""" + """Get all pull requests across the project's repositories. + + Returns: + QuerySet[PullRequest]: A queryset of pull requests with related data. + + """ return ( PullRequest.objects.filter( repository__in=self.repositories.all(), @@ -278,19 +363,34 @@ def pull_requests(self): @property def pull_requests_count(self) -> int: - """Return count of pull requests.""" + """Get the total number of pull requests. + + Returns: + int: Count of pull requests across the project's repositories. + + """ return self.pull_requests.count() @property def pull_request_last_created_at(self) -> datetime.datetime | None: - """Return last created pull request.""" + """Get the most recent pull request creation timestamp. + + Returns: + datetime.datetime | None: Latest `created_at` timestamp, or None. + + """ return self.pull_requests.aggregate( models.Max("created_at"), )["created_at__max"] @property def published_releases(self): - """Return project releases.""" + """Get published releases across the project's repositories. + + Returns: + QuerySet[Release]: A queryset of non-draft releases with related data. + + """ return Release.objects.filter( is_draft=False, published_at__isnull=False, @@ -303,7 +403,12 @@ def published_releases(self): @property def recent_milestones(self): - """Return recent milestones.""" + """Get milestones across the project's repositories. + + Returns: + QuerySet[Milestone]: A queryset of milestones with related data. + + """ return ( Milestone.objects.filter( repository__in=self.repositories.all(), @@ -319,7 +424,12 @@ def recent_milestones(self): @property def recent_releases_count(self) -> int: - """Return count of recent releases per a specific period.""" + """Get the number of releases published in the last 60 days. + + Returns: + int: Count of releases published within the recent 60-day period. + + """ recent_period = timezone.now() - datetime.timedelta(days=60) return self.published_releases.filter( published_at__gte=recent_period, @@ -327,17 +437,32 @@ def recent_releases_count(self) -> int: @property def repositories_count(self) -> int: - """Return count of repositories.""" + """Get the number of repositories associated with this project. + + Returns: + int: Count of repositories linked to the project. + + """ return self.repositories.count() @property def unanswered_issues_count(self) -> int: - """Return count of unanswered issues.""" + """Get the number of issues with no comments. + + Returns: + int: Count of issues where `comments_count` equals zero. + + """ return self.issues.filter(comments_count=0).count() @property def unassigned_issues_count(self) -> int: - """Return count of unassigned issues.""" + """Get the number of issues with no assignees. + + Returns: + int: Count of issues where no user is assigned. + + """ return self.issues.filter(assignees__isnull=True).count() def deactivate(self) -> None: From 3d2e16498b891020a44637b8b81f0342b0066c50 Mon Sep 17 00:00:00 2001 From: Moiz Zulfiqar Date: Mon, 26 Jan 2026 15:13:51 +0500 Subject: [PATCH 2/5] Add docstrings to Django admin methods --- backend/apps/owasp/admin/entity_channel.py | 37 +++++++- backend/apps/owasp/admin/entity_member.py | 47 +++++++++- backend/apps/owasp/admin/member_profile.py | 13 ++- backend/apps/owasp/admin/member_snapshot.py | 13 ++- backend/apps/owasp/admin/mixins.py | 89 +++++++++++++++++-- backend/apps/owasp/admin/project.py | 13 ++- .../owasp/admin/project_health_metrics.py | 10 ++- backend/apps/owasp/admin/widgets.py | 26 +++++- backend/apps/slack/admin/member.py | 15 +++- 9 files changed, 241 insertions(+), 22 deletions(-) diff --git a/backend/apps/owasp/admin/entity_channel.py b/backend/apps/owasp/admin/entity_channel.py index 2f2bcf3c86..8b0dd45a2b 100644 --- a/backend/apps/owasp/admin/entity_channel.py +++ b/backend/apps/owasp/admin/entity_channel.py @@ -9,7 +9,14 @@ @admin.action(description="Mark selected EntityChannels as reviewed") def mark_as_reviewed(_modeladmin, request, queryset): - """Admin action to mark selected EntityChannels as reviewed.""" + """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.", @@ -62,7 +69,18 @@ class EntityChannelAdmin(admin.ModelAdmin): ) def channel_search_display(self, obj): - """Display the channel name for the selected channel.""" + """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": @@ -75,7 +93,20 @@ 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.""" + """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..119b8e9a54 100644 --- a/backend/apps/owasp/admin/entity_member.py +++ b/backend/apps/owasp/admin/entity_member.py @@ -42,7 +42,15 @@ class EntityMemberAdmin(admin.ModelAdmin): @admin.action(description="Approve selected members") def approve_members(self, request, queryset): - """Approve selected members.""" + """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.", @@ -50,7 +58,15 @@ def approve_members(self, request, queryset): @admin.display(description="Entity", ordering="entity_type") def entity(self, obj): - """Return entity link.""" + """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( '{}', @@ -66,7 +82,15 @@ def entity(self, obj): @admin.display(description="OWASP URL", ordering="entity_type") def owasp_url(self, obj): - """Return entity OWASP site URL.""" + """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 @@ -74,7 +98,22 @@ def owasp_url(self, obj): ) def get_search_results(self, request, queryset, search_term): - """Get search results from entity name or key.""" + """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..c08c57236b 100644 --- a/backend/apps/owasp/admin/member_profile.py +++ b/backend/apps/owasp/admin/member_profile.py @@ -72,7 +72,18 @@ class MemberProfileAdmin(admin.ModelAdmin): ) def get_queryset(self, request): - """Optimize queryset with select_related.""" + """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. + + """ 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..7d8f53dba8 100644 --- a/backend/apps/owasp/admin/member_snapshot.py +++ b/backend/apps/owasp/admin/member_snapshot.py @@ -108,7 +108,18 @@ class MemberSnapshotAdmin(admin.ModelAdmin): ) def get_queryset(self, request): - """Optimize queryset with select_related.""" + """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 prefetched github_user relations. + + """ 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 59b7839484..3697012511 100644 --- a/backend/apps/owasp/admin/mixins.py +++ b/backend/apps/owasp/admin/mixins.py @@ -26,7 +26,15 @@ class BaseOwaspAdminMixin: ) def get_base_list_display(self, *additional_fields): - """Get base list display with additional fields.""" + """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. + + """ return tuple( ("name",) if hasattr(self.model, "name") else (), *additional_fields, @@ -34,7 +42,15 @@ def get_base_list_display(self, *additional_fields): ) def get_base_search_fields(self, *additional_fields): - """Get base search fields with additional fields.""" + """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. + + """ return self.search_field_names + additional_fields @@ -80,7 +96,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": @@ -96,11 +125,27 @@ class GenericEntityAdminMixin(BaseOwaspAdminMixin): """Mixin for generic entity admin with common entity functionality.""" def get_queryset(self, request): - """Get queryset with optimized relations.""" + """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") def custom_field_github_urls(self, obj): - """Entity GitHub URLs with uniform formatting.""" + """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 "" @@ -113,14 +158,30 @@ def custom_field_github_urls(self, obj): ) def custom_field_owasp_url(self, obj): - """Entity OWASP URL with uniform formatting.""" + """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.""" + """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: @@ -144,7 +205,19 @@ 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.""" + """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..6142b33ea0 100644 --- a/backend/apps/owasp/admin/project_health_metrics.py +++ b/backend/apps/owasp/admin/project_health_metrics.py @@ -29,7 +29,15 @@ class ProjectHealthMetricsAdmin(admin.ModelAdmin, StandardOwaspAdminMixin): search_fields = ("project__name",) def project(self, obj): - """Display project name.""" + """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..2811660ffe 100644 --- a/backend/apps/owasp/admin/widgets.py +++ b/backend/apps/owasp/admin/widgets.py @@ -8,12 +8,34 @@ class ChannelIdWidget(forms.TextInput): """Custom widget for channel_id with search functionality.""" def __init__(self, *args, **kwargs): - """Initialize the widget with custom attributes.""" + """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.""" + """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..a111228a56 100644 --- a/backend/apps/slack/admin/member.py +++ b/backend/apps/slack/admin/member.py @@ -24,7 +24,20 @@ class MemberAdmin(admin.ModelAdmin): ) def approve_suggested_users(self, request, queryset): - """Approve all suggested users for selected members, enforcing one-to-one constraints.""" + """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() From 48686857595ee88e52d9e982e07379213ac1a371 Mon Sep 17 00:00:00 2001 From: Moiz Zulfiqar Date: Tue, 27 Jan 2026 16:14:38 +0500 Subject: [PATCH 3/5] CR's comments addressed --- backend/apps/github/models/mixins/repository.py | 2 +- backend/apps/owasp/admin/member_snapshot.py | 2 +- backend/apps/owasp/admin/mixins.py | 7 ++----- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/backend/apps/github/models/mixins/repository.py b/backend/apps/github/models/mixins/repository.py index 63767cd05a..7c125a124f 100644 --- a/backend/apps/github/models/mixins/repository.py +++ b/backend/apps/github/models/mixins/repository.py @@ -189,7 +189,7 @@ def idx_subscribers_count(self) -> int: int: The count of users watching this repository. """ - return self.stars_count + return self.subscribers_count @property def idx_top_contributors(self) -> list[dict[str, Any]]: diff --git a/backend/apps/owasp/admin/member_snapshot.py b/backend/apps/owasp/admin/member_snapshot.py index 7d8f53dba8..6e147fe835 100644 --- a/backend/apps/owasp/admin/member_snapshot.py +++ b/backend/apps/owasp/admin/member_snapshot.py @@ -117,7 +117,7 @@ def get_queryset(self, request): request: The HTTP request object. Returns: - QuerySet: MemberSnapshot queryset with prefetched github_user relations. + QuerySet: MemberSnapshot queryset with select_related("github_user") applied. """ queryset = super().get_queryset(request) diff --git a/backend/apps/owasp/admin/mixins.py b/backend/apps/owasp/admin/mixins.py index 3697012511..0725036789 100644 --- a/backend/apps/owasp/admin/mixins.py +++ b/backend/apps/owasp/admin/mixins.py @@ -35,11 +35,8 @@ def get_base_list_display(self, *additional_fields): tuple: Base display fields (name, timestamps) plus additional fields. """ - return tuple( - ("name",) if hasattr(self.model, "name") else (), - *additional_fields, - *self.list_display_field_names, - ) + 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): """Build base search fields tuple with additional fields. From 1844aa25935b8264bd7692f0d78b8e9853ff2d6d Mon Sep 17 00:00:00 2001 From: Moiz Zulfiqar Date: Wed, 28 Jan 2026 01:51:22 +0500 Subject: [PATCH 4/5] Restore non-admin files to match main --- backend/apps/github/models/mixins/repository.py | 12 ++++++------ backend/apps/github/models/mixins/user.py | 10 +++------- backend/apps/owasp/models/project.py | 14 +++++++------- 3 files changed, 16 insertions(+), 20 deletions(-) diff --git a/backend/apps/github/models/mixins/repository.py b/backend/apps/github/models/mixins/repository.py index 264bc28f8b..5586d1e9c0 100644 --- a/backend/apps/github/models/mixins/repository.py +++ b/backend/apps/github/models/mixins/repository.py @@ -52,7 +52,7 @@ def idx_created_at(self) -> float: """Get the repository creation timestamp for indexing. Returns: - float: Unix timestamp (seconds since epoch) when the repository was created. + float: Unix timestamp when the repository was created. """ return self.created_at.timestamp() @@ -112,7 +112,7 @@ def idx_license(self) -> str: """Get the repository's license identifier for indexing. Returns: - str: The SPDX license identifier or name, if available. + str: The license identifier. """ return self.license @@ -153,7 +153,7 @@ def idx_pushed_at(self) -> float: """Get the timestamp of the last push to this repository for indexing. Returns: - float: Unix timestamp (seconds since epoch) of the most recent push. + float: Unix timestamp of the most recent push. """ return self.pushed_at.timestamp() @@ -170,17 +170,17 @@ def idx_size(self) -> int: @property def idx_stars_count(self) -> int: - """Get the total number of stars (likes) this repository has received for indexing. + """Get the total number of stars this repository has received for indexing. Returns: - int: The total count of stars/favorites on the repository. + int: The total count of stars on the repository. """ return self.stars_count @property def idx_subscribers_count(self) -> int: - """Get the total number of watchers/subscribers for this repository for indexing. + """Get the total number of subscribers for this repository for indexing. Returns: int: The count of users watching this repository. diff --git a/backend/apps/github/models/mixins/user.py b/backend/apps/github/models/mixins/user.py index a8f1902744..ed25aedc26 100644 --- a/backend/apps/github/models/mixins/user.py +++ b/backend/apps/github/models/mixins/user.py @@ -14,10 +14,6 @@ class UserIndexMixin: def is_indexable(self): """Determine if the user should be indexed. - A user is considered indexable if they are not a bot, their login does not - end with common bot suffixes ("Bot" or "-bot"), and their login is not - present in the non-indexable logins list. - Returns: bool: True if the user meets all indexing criteria, False otherwise. @@ -73,7 +69,7 @@ def idx_created_at(self) -> float: """Get the account creation timestamp for indexing. Returns: - float: Unix timestamp (seconds since epoch) when the account was created. + float: Unix timestamp when the account was created. """ return self.created_at.timestamp() @@ -163,7 +159,7 @@ def idx_title(self) -> str: """Get the user's profile title or headline for indexing. Returns: - str: The title or headline associated with the user, if any. + str: The title or headline associated with the user. """ return self.title @@ -256,7 +252,7 @@ def idx_issues_count(self) -> int: @property def idx_releases(self) -> list[dict]: - """Get recent releases associated with the user for indexing. + """Get releases associated with the user for indexing. Returns: list[dict]: A list of release summaries including pre-release flag, diff --git a/backend/apps/owasp/models/project.py b/backend/apps/owasp/models/project.py index 5d65eb0c8b..37305845b7 100644 --- a/backend/apps/owasp/models/project.py +++ b/backend/apps/owasp/models/project.py @@ -231,7 +231,7 @@ def is_tool_type(self) -> bool: @property def issues(self): - """Get all issues across the project's repositories. + """Get issues across the project's repositories. Returns: QuerySet[Issue]: A queryset of issues with related entities prefetched. @@ -305,7 +305,7 @@ def nest_url(self) -> str: @property def open_issues(self): - """Get all open issues across the project's repositories. + """Get open issues across the project's repositories. Returns: QuerySet[Issue]: A queryset of open issues with repository related data. @@ -339,7 +339,7 @@ def owasp_page_last_updated_at(self) -> datetime.datetime | None: @property def pull_requests(self): - """Get all pull requests across the project's repositories. + """Get pull requests across the project's repositories. Returns: QuerySet[PullRequest]: A queryset of pull requests with related data. @@ -384,7 +384,7 @@ def pull_request_last_created_at(self) -> datetime.datetime | None: )["created_at__max"] @property - def published_releases(self): + def published_releases(self) -> models.QuerySet[Release]: """Get published releases across the project's repositories. Returns: @@ -402,7 +402,7 @@ def published_releases(self): ) @property - def recent_milestones(self): + def recent_milestones(self) -> models.QuerySet[Milestone]: """Get milestones across the project's repositories. Returns: @@ -424,10 +424,10 @@ def recent_milestones(self): @property def recent_releases_count(self) -> int: - """Get the number of releases published in the last 60 days. + """Get the number of recent releases. Returns: - int: Count of releases published within the recent 60-day period. + int: Count of releases published recently. """ recent_period = timezone.now() - datetime.timedelta(days=60) From aade1958859c0cd9beeb7d6e51499789a8c83327 Mon Sep 17 00:00:00 2001 From: Arkadii Yakovets Date: Wed, 28 Jan 2026 23:25:17 -0800 Subject: [PATCH 5/5] Update code --- backend/apps/owasp/admin/entity_channel.py | 7 ++++--- backend/apps/owasp/admin/entity_member.py | 9 +++++---- backend/apps/owasp/admin/member_profile.py | 6 +++--- backend/apps/owasp/admin/member_snapshot.py | 6 +++--- backend/apps/owasp/admin/mixins.py | 17 +++++++++-------- .../apps/owasp/admin/project_health_metrics.py | 2 +- backend/apps/owasp/admin/widgets.py | 4 ++-- backend/apps/slack/admin/member.py | 2 +- 8 files changed, 28 insertions(+), 25 deletions(-) diff --git a/backend/apps/owasp/admin/entity_channel.py b/backend/apps/owasp/admin/entity_channel.py index 8b0dd45a2b..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,7 +9,7 @@ @admin.action(description="Mark selected EntityChannels as reviewed") -def mark_as_reviewed(_modeladmin, request, queryset): +def mark_as_reviewed(_modeladmin, request, queryset) -> None: """Admin action to mark selected EntityChannels as reviewed. Args: @@ -68,7 +69,7 @@ class EntityChannelAdmin(admin.ModelAdmin): "channel_id", ) - def channel_search_display(self, obj): + 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 @@ -92,7 +93,7 @@ def channel_search_display(self, obj): channel_search_display.short_description = "Channel Name" - def get_form(self, request, obj=None, **kwargs): + 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 diff --git a/backend/apps/owasp/admin/entity_member.py b/backend/apps/owasp/admin/entity_member.py index 119b8e9a54..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,7 +42,7 @@ class EntityMemberAdmin(admin.ModelAdmin): ordering = ("member__name", "order") @admin.action(description="Approve selected members") - def approve_members(self, request, queryset): + def approve_members(self, request, queryset) -> None: """Admin action to approve selected members. Sets is_active and is_reviewed flags for selected entity members. @@ -57,7 +58,7 @@ def approve_members(self, request, queryset): ) @admin.display(description="Entity", ordering="entity_type") - def entity(self, obj): + def entity(self, obj) -> str: """Display entity as a link in the admin list view. Args: @@ -81,7 +82,7 @@ def entity(self, obj): ) @admin.display(description="OWASP URL", ordering="entity_type") - def owasp_url(self, obj): + def owasp_url(self, obj) -> str: """Display entity OWASP website link in admin list view. Args: @@ -97,7 +98,7 @@ def owasp_url(self, obj): else "-" ) - def get_search_results(self, request, queryset, search_term): + 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 diff --git a/backend/apps/owasp/admin/member_profile.py b/backend/apps/owasp/admin/member_profile.py index c08c57236b..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,7 +72,7 @@ class MemberProfileAdmin(admin.ModelAdmin): ), ) - def get_queryset(self, request): + 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 @@ -84,8 +85,7 @@ def get_queryset(self, request): QuerySet: MemberProfile queryset with prefetched github_user relations. """ - queryset = super().get_queryset(request) - return queryset.select_related("github_user") + 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 6e147fe835..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,7 +108,7 @@ class MemberSnapshotAdmin(admin.ModelAdmin): ), ) - def get_queryset(self, request): + 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 @@ -120,8 +121,7 @@ def get_queryset(self, request): QuerySet: MemberSnapshot queryset with select_related("github_user") applied. """ - queryset = super().get_queryset(request) - return queryset.select_related("github_user") + 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 0725036789..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,7 +26,7 @@ class BaseOwaspAdminMixin: "key", ) - def get_base_list_display(self, *additional_fields): + def get_base_list_display(self, *additional_fields) -> tuple[str, ...]: """Build base list display tuple with additional fields. Args: @@ -38,7 +39,7 @@ def get_base_list_display(self, *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): + def get_base_search_fields(self, *additional_fields) -> tuple[str, ...]: """Build base search fields tuple with additional fields. Args: @@ -121,7 +122,7 @@ 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): + def get_queryset(self, request) -> models.QuerySet: """Retrieve optimized queryset with prefetched repositories. Args: @@ -131,9 +132,9 @@ def get_queryset(self, request): QuerySet: Queryset with prefetched repositories for efficient display. """ - return super().get_queryset(request).prefetch_related("repositories") + return super().get_queryset(request).prefetch_related("repositories") # type: ignore[misc] - def custom_field_github_urls(self, obj): + def custom_field_github_urls(self, obj) -> str: """Display entity GitHub repository links in admin list view. Args: @@ -154,7 +155,7 @@ def custom_field_github_urls(self, obj): " ".join(links) ) - def custom_field_owasp_url(self, obj): + def custom_field_owasp_url(self, obj) -> str: """Display entity OWASP website link in admin list view. Args: @@ -169,7 +170,7 @@ def custom_field_owasp_url(self, obj): return format_html("↗️", obj.key) - def _format_github_link(self, repository): + def _format_github_link(self, repository) -> str: """Format a GitHub repository link as HTML. Args: @@ -201,7 +202,7 @@ class StandardOwaspAdminMixin(BaseOwaspAdminMixin): def get_common_config( self, extra_list_display=None, extra_search_fields=None, extra_list_filters=None - ): + ) -> dict: """Build common admin configuration dictionary. Reduces boilerplate by merging base fields with custom additions. diff --git a/backend/apps/owasp/admin/project_health_metrics.py b/backend/apps/owasp/admin/project_health_metrics.py index 6142b33ea0..16fd662389 100644 --- a/backend/apps/owasp/admin/project_health_metrics.py +++ b/backend/apps/owasp/admin/project_health_metrics.py @@ -28,7 +28,7 @@ class ProjectHealthMetricsAdmin(admin.ModelAdmin, StandardOwaspAdminMixin): ) search_fields = ("project__name",) - def project(self, obj): + def project(self, obj) -> str: """Display project name in admin list view. Args: diff --git a/backend/apps/owasp/admin/widgets.py b/backend/apps/owasp/admin/widgets.py index 2811660ffe..bf56e80934 100644 --- a/backend/apps/owasp/admin/widgets.py +++ b/backend/apps/owasp/admin/widgets.py @@ -7,7 +7,7 @@ class ChannelIdWidget(forms.TextInput): """Custom widget for channel_id with search functionality.""" - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: """Initialize the ChannelIdWidget. Sets up custom CSS classes and placeholder text for the text input field. @@ -20,7 +20,7 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.attrs.update({"class": "vForeignKeyRawIdAdminField", "placeholder": "Channel ID"}) - def render(self, name, value, attrs=None, renderer=None): + 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 diff --git a/backend/apps/slack/admin/member.py b/backend/apps/slack/admin/member.py index a111228a56..e658ae2750 100644 --- a/backend/apps/slack/admin/member.py +++ b/backend/apps/slack/admin/member.py @@ -23,7 +23,7 @@ class MemberAdmin(admin.ModelAdmin): "user__login", ) - def approve_suggested_users(self, request, queryset): + 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,