-
Notifications
You must be signed in to change notification settings - Fork 3
Fix discord role apply gating #201
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
28f8cf8
91728db
2892fbd
d462f40
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1023,7 +1023,9 @@ async def on_submit(self, interaction: discord.Interaction) -> None: | |
| self.confirmation_view.discord_role_suggestions = normalized | ||
| for item in self.confirmation_view.children: | ||
| if isinstance(item, ResumeApplyDiscordRolesButton): | ||
| item.disabled = not bool(normalized) | ||
| item.disabled = not ( | ||
| self.confirmation_view.can_apply_discord_roles and bool(normalized) | ||
| ) | ||
|
|
||
| if normalized: | ||
| count = len(normalized) | ||
|
|
@@ -1269,11 +1271,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, | ||
| ) | ||
|
|
||
| async def callback(self, interaction: discord.Interaction) -> None: | ||
|
|
@@ -1306,6 +1309,18 @@ def _audit_apply_roles_event( | |
| resource_id=view.contact_id, | ||
| ) | ||
|
|
||
| if not view.can_apply_discord_roles: | ||
| _audit_apply_roles_event( | ||
| "denied", | ||
| "missing_linked_user", | ||
| {"reason": "button_disabled_for_role_application"}, | ||
| ) | ||
| await interaction.response.send_message( | ||
| "❌ Discord roles can only be applied after linking this contact to a Discord user.", | ||
| ephemeral=True, | ||
| ) | ||
| return | ||
|
|
||
| if not interaction.guild: | ||
| await interaction.response.send_message( | ||
| "❌ Discord roles can only be managed inside a server.", | ||
|
|
@@ -1605,6 +1620,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 | ||
|
|
@@ -1615,6 +1631,11 @@ 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 = bool( | ||
| can_apply_discord_roles | ||
| or discord_role_target_user_id | ||
| or (isinstance(link_discord, dict) and bool(link_discord.get("user_id"))) | ||
| ) | ||
|
Comment on lines
+1634
to
+1638
|
||
| self.discord_role_suggestions = list( | ||
| dict.fromkeys(discord_role_suggestions or []) | ||
| ) | ||
|
|
@@ -1637,7 +1658,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) | ||
| ) | ||
|
Comment on lines
+1661
to
+1663
|
||
| self.add_item(ResumeEditLocationButton()) | ||
|
|
||
| def _set_seniority_override(self, value: str) -> str: | ||
|
|
@@ -3667,6 +3690,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. | ||
|
|
||
|
|
@@ -3689,6 +3713,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, | ||
| ) | ||
|
Comment on lines
+3716
to
+3725
|
||
|
|
||
| if technical: | ||
| embed.add_field( | ||
|
|
@@ -3890,6 +3924,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 | ||
| ): | ||
|
|
@@ -3932,10 +3967,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: | ||
|
|
@@ -3970,6 +4007,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( | ||
|
|
@@ -4015,6 +4053,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( | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This new early-return branch for unlinked contacts is important behavior (and was added specifically to guard a regression), but there’s no unit test covering the callback being triggered while
view.can_apply_discord_rolesis false (e.g., via a stale interaction/custom_id). Adding a test that asserts the ephemeral error message (and, if possible, that audit is called) would help prevent this from regressing again.