Skip to content
Open
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
32 changes: 30 additions & 2 deletions apps/discord_bot/src/five08/discord_bot/cogs/crm.py
Original file line number Diff line number Diff line change
Expand Up @@ -1269,11 +1269,12 @@ async def callback(self, interaction: discord.Interaction) -> None:
class ResumeApplyDiscordRolesButton(discord.ui.Button["ResumeUpdateConfirmationView"]):
"""Button that applies suggested Discord roles to the linked member."""

def __init__(self) -> None:
def __init__(self, *, disabled: bool = False) -> None:
super().__init__(
label="Apply Discord Roles",
style=discord.ButtonStyle.success,
custom_id="resume_apply_discord_roles",
disabled=disabled,
)
Comment on lines +1272 to 1278
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

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

In ResumeEditDiscordRolesModal.on_submit (line 1026, outside the diff), the apply button's disabled state is recomputed as not bool(normalized), which ignores can_apply_discord_roles. If a user with an unlinked contact opens and saves the Edit Discord Roles modal with at least one role, the button will be visually re-enabled even though it was intentionally disabled due to no Discord link. The disabled computation there should also check confirmation_view.can_apply_discord_roles, e.g. not bool(normalized) or not self.confirmation_view.can_apply_discord_roles. Although the server-side guard at line 1288 still protects against the action being carried out, the UI state becomes inconsistent with the intent of this PR.

Copilot uses AI. Check for mistakes.

async def callback(self, interaction: discord.Interaction) -> None:
Expand All @@ -1284,6 +1285,13 @@ async def callback(self, interaction: discord.Interaction) -> None:
)
return

if not view.can_apply_discord_roles:
await interaction.response.send_message(
"❌ Discord roles can only be applied after linking this contact to a Discord user.",
ephemeral=True,
)
return

target_user_id_raw: str | None = None

def _audit_apply_roles_event(
Expand Down Expand Up @@ -1605,6 +1613,7 @@ def __init__(
parsed_seniority: str | None = None,
discord_role_suggestions: list[str] | None = None,
discord_role_target_user_id: str | None = None,
can_apply_discord_roles: bool = False,
) -> None:
super().__init__(timeout=300)
self.crm_cog = crm_cog
Expand All @@ -1615,6 +1624,7 @@ def __init__(
self.link_discord = link_discord
self.parsed_seniority = parsed_seniority
self.discord_role_target_user_id = discord_role_target_user_id
self.can_apply_discord_roles = can_apply_discord_roles
self.discord_role_suggestions = list(
dict.fromkeys(discord_role_suggestions or [])
)
Expand All @@ -1637,7 +1647,9 @@ def __init__(
self.add_item(ResumeEditRolesButton())
if self.discord_role_suggestions:
self.add_item(ResumeEditDiscordRolesButton())
self.add_item(ResumeApplyDiscordRolesButton())
self.add_item(
ResumeApplyDiscordRolesButton(disabled=not self.can_apply_discord_roles)
)
self.add_item(ResumeEditLocationButton())

def _set_seniority_override(self, value: str) -> str:
Expand Down Expand Up @@ -3667,6 +3679,7 @@ def _build_role_suggestions_embed(
locality_roles: list[str] | None = None,
extracted_profile: dict[str, Any] | None = None,
current_discord_roles: list[str] | None = None,
can_apply_discord_roles: bool = False,
) -> discord.Embed | None:
"""Build a separate embed suggesting Discord roles to add based on resume data.

Expand All @@ -3689,6 +3702,16 @@ def _build_role_suggestions_embed(
description=f"Roles to **add** for **{contact_name}** based on resume — never remove existing roles.",
color=0x57F287,
)
if not can_apply_discord_roles:
embed.add_field(
name="🔒 Link required",
value=(
"This contact is not currently linked to a Discord user, so suggested roles "
"cannot be applied automatically. Link them first in CRM or use "
"`/link-discord-user`."
),
inline=False,
)

if technical:
embed.add_field(
Expand Down Expand Up @@ -3890,6 +3913,7 @@ async def _run_resume_extract_and_preview(
role_suggestions_embed: discord.Embed | None = None
suggested_discord_roles: list[str] = []
discord_role_target_user_id: str | None = None
can_apply_discord_roles = False
if action_name == "crm.reprocess_resume" or (
action_name == "crm.upload_resume" and link_member
):
Expand Down Expand Up @@ -3932,10 +3956,12 @@ async def _run_resume_extract_and_preview(
suggested_discord_roles = list(
dict.fromkeys(technical_suggestions + locality_suggestions)
)
can_apply_discord_roles = bool(discord_role_target_user_id or link_member)
role_suggestions_embed = self._build_role_suggestions_embed(
contact_name=contact_name,
technical_roles=technical_suggestions,
locality_roles=locality_suggestions,
can_apply_discord_roles=can_apply_discord_roles,
)

if not proposed_updates and not link_member and not parsed_seniority:
Expand Down Expand Up @@ -3970,6 +3996,7 @@ async def _run_resume_extract_and_preview(
parsed_seniority=parsed_seniority,
discord_role_suggestions=suggested_discord_roles,
discord_role_target_user_id=discord_role_target_user_id,
can_apply_discord_roles=can_apply_discord_roles,
)
if action_name != "crm.reprocess_resume":
self._audit_command(
Expand Down Expand Up @@ -4015,6 +4042,7 @@ async def _run_resume_extract_and_preview(
parsed_seniority=parsed_seniority,
discord_role_suggestions=suggested_discord_roles,
discord_role_target_user_id=discord_role_target_user_id,
can_apply_discord_roles=can_apply_discord_roles,
)
if action_name != "crm.reprocess_resume":
self._audit_command(
Expand Down
Loading