Feature/capr 44 integrate post event rating feedback system#135
Feature/capr 44 integrate post event rating feedback system#135
Conversation
…ild-settings-user-experience-improvement
- Add EventFeedback cog with /event_feedback slash command - DM all guild members a 1-10 rating view after an event - Prompt improvement suggestion via modal for ratings below 6 - Add EventFeedbackSchema (Pydantic) for rating + suggestion - Store responses in-memory with DB call markers for future persistence
…o feature/capr-44-integrate-post-event-rating-feedback-system
…o feature/capr-44-integrate-post-event-rating-feedback-system
Reviewer's GuideAdds a new event feedback Discord cog that DMs members a post-event rating survey with optional improvement text, stores responses in-memory, and exposes slash commands to send surveys and view aggregated feedback, plus a test-only user targeting config flag. Sequence diagram for post-event feedback DM flowsequenceDiagram
actor Admin
participant DiscordServer
participant EventFeedback
participant GuildMembers
participant MemberDM as Member_DM
Admin->>DiscordServer: /event_feedback event_name
DiscordServer->>EventFeedback: event_feedback(interaction, event_name)
EventFeedback->>EventFeedback: _normalize_event_name()
EventFeedback->>DiscordServer: interaction.response.defer(ephemeral=True)
EventFeedback->>DiscordServer: validate guild is not None
EventFeedback->>EventFeedback: _ensure_feedback_stores(guild_id, event_name)
EventFeedback->>EventFeedback: _resolve_target_members(interaction, guild)
EventFeedback->>GuildMembers: iterate members
loop for each member
EventFeedback->>MemberDM: member.send(content, RatingView)
MemberDM-->>EventFeedback: dm_message
EventFeedback->>EventFeedback: pending_feedback_context_by_user[user_id] = (guild_id, event_name)
EventFeedback->>EventFeedback: feedback_user_display_names[guild_id][user_id] = display_name
end
EventFeedback->>DiscordServer: interaction.followup.send(summary, ephemeral=True)
actor Member
Member->>MemberDM: click RatingRangeButton
MemberDM->>EventFeedback: RatingRangeButton.callback(interaction)
alt average_rating >= _POSITIVE_THRESHOLD
EventFeedback->>RatingView: set responded = True, disable_all_items()
EventFeedback->>MemberDM: interaction.response.edit_message(view)
EventFeedback->>EventFeedback: save_feedback(interaction, rating, None)
else average_rating < _POSITIVE_THRESHOLD
EventFeedback->>MemberDM: interaction.response.send_modal(ImprovementModal)
Member->>MemberDM: submit ImprovementModal
MemberDM->>EventFeedback: ImprovementModal.on_submit(interaction)
EventFeedback->>EventFeedback: save_feedback(interaction, rating, suggestion)
EventFeedback->>RatingView: set responded = True, disable_all_items()
EventFeedback->>MemberDM: dm_message.edit(view=None)
end
EventFeedback->>MemberDM: interaction.response.send_message("Response recorded")
Updated class diagram for event feedback cog and related typesclassDiagram
class Settings {
+announcement_channel_name str
+test_user_id int | None
}
class EventFeedback {
+bot commands.Bot
+log logging.Logger
+dm_service DirectMessenger
+feedback_data dict[int, dict[str, dict[int, EventFeedbackSchema]]]
+pending_feedback_context_by_user dict[int, tuple[int, str]]
+feedback_user_display_names dict[int, dict[int, str]]
+__init__(bot commands.Bot)
+send_feedback_dm(guild discord.Guild, member discord.Member, content str) async
+collect_feedback(guild discord.Guild, members list[discord.Member], content str) async
+_ensure_feedback_stores(guild_id int, event_name str) void
+_default_event_name() str
+_resolve_target_members(interaction discord.Interaction, guild discord.Guild) list[discord.Member] | None async
+_normalize_event_name(event_name str) str | None
+event_feedback(interaction discord.Interaction, event_name str) async
+_send_event_feedback(interaction discord.Interaction, event_name str) async
+_rating_to_label(rating int) str
+view_feedback(interaction discord.Interaction, event_name str) async
+view_feedback_event_name_autocomplete(interaction discord.Interaction, current str) list[app_commands.Choice[str]] async
+_view_event_feedback(interaction discord.Interaction, event_name str) async
+save_feedback(interaction discord.Interaction, rating int, suggestion str | None) async
}
class RatingView {
+cog EventFeedback
+responded bool
+dm_message discord.Message | None
+__init__(cog EventFeedback)
}
class RatingRangeButton {
+rating_range tuple[int, int]
+__init__(label str, style discord.ButtonStyle, rating_range tuple[int, int])
+callback(interaction discord.Interaction) async
+cog EventFeedback
}
class ImprovementModal {
+cog EventFeedback
+rating int
+dm_message discord.Message | None
+view RatingView | None
+suggestion ui.TextInput
+__init__(cog EventFeedback, rating int, dm_message discord.Message | None, view RatingView | None)
+on_submit(interaction discord.Interaction) async
}
class EventSelect {
+__init__()
+callback(interaction discord.Interaction) async
}
class EventSelectView {
+cog EventFeedback
+__init__(cog EventFeedback)
}
class ViewEventSelect {
+__init__()
+callback(interaction discord.Interaction) async
}
class ViewEventSelectView {
+cog EventFeedback
+__init__(cog EventFeedback)
}
class EventFeedbackSchema {
+rating int
+improvement_suggestion str | None
}
class DirectMessenger {
+compose_to_user(guild discord.Guild, user_id int, content str, policy Policy) async
+send(guild discord.Guild, draft any) async
}
class Policy {
+max_recipients int
}
class BaseView {
}
class BaseModal {
}
Settings ..> EventFeedback : uses test_user_id
EventFeedback o--> EventFeedbackSchema : stores
EventFeedback o--> RatingView : creates
EventFeedback o--> DirectMessenger : uses
EventFeedback ..> Policy : uses
RatingView --|> BaseView
RatingRangeButton --|> ui.Button
RatingView o--> RatingRangeButton : contains
RatingRangeButton --> EventFeedback : accesses cog
ImprovementModal --|> BaseModal
ImprovementModal --> EventFeedback : calls save_feedback
ImprovementModal --> RatingView : disables_on_submit
EventSelect --|> ui.Select
EventSelectView --|> BaseView
EventSelectView o--> EventSelect
ViewEventSelect --|> ui.Select
ViewEventSelectView --|> BaseView
ViewEventSelectView o--> ViewEventSelect
EventFeedback --> EventSelectView : may use in future
EventFeedback --> ViewEventSelectView : may use in future
EventFeedback --> Settings : reads
EventFeedback --> discord.Interaction
EventFeedback --> discord.Guild
EventFeedback --> discord.Member
EventFeedbackSchema --|> BaseModel
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 3 issues, and left some high level feedback:
pending_feedback_context_by_useris never cleared after a user submits feedback, so consider removing the entry on successful save to avoid stale context and potential confusion if you later support multiple concurrent events per user._default_event_name,collect_feedback, and theEventSelect/ViewEventSelectviews are currently unused scaffolding; either wire them into commands or remove them to keep the extension focused and reduce maintenance overhead.- When saving feedback in
save_feedback, you rebuild the nestedfeedback_data[guild_id][event_name]structure rather than using_ensure_feedback_stores; reusing that helper would keep the initialization logic in one place.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- `pending_feedback_context_by_user` is never cleared after a user submits feedback, so consider removing the entry on successful save to avoid stale context and potential confusion if you later support multiple concurrent events per user.
- `_default_event_name`, `collect_feedback`, and the `EventSelect`/`ViewEventSelect` views are currently unused scaffolding; either wire them into commands or remove them to keep the extension focused and reduce maintenance overhead.
- When saving feedback in `save_feedback`, you rebuild the nested `feedback_data[guild_id][event_name]` structure rather than using `_ensure_feedback_stores`; reusing that helper would keep the initialization logic in one place.
## Individual Comments
### Comment 1
<location path="capy_discord/exts/event_feedback/event_feedback.py" line_range="106-107" />
<code_context>
+ await interaction.response.send_message("You've already submitted your rating - thanks!", ephemeral=True)
+ return
+
+ # Use the average of the range as the representative rating.
+ average_rating = sum(self.rating_range) // 2
+
+ if average_rating >= _POSITIVE_THRESHOLD:
</code_context>
<issue_to_address>
**issue (bug_risk):** Using the range average conflicts with the documented positive threshold and can misclassify ratings.
The module docstring says ratings at or above `_POSITIVE_THRESHOLD` (currently 6) skip the follow-up question, but using the average of the range means the `4–6` button always maps to `5`, which is treated as negative. Someone intending to pick 6 will still see the improvement modal. Consider either mapping each button to a specific rating (e.g., `1`, `5`, `8`), asking for an exact rating in the modal when ranges are used, or adjusting the threshold/range mapping so the behavior matches the documented semantics and UX copy.
</issue_to_address>
### Comment 2
<location path="capy_discord/exts/event_feedback/event_feedback.py" line_range="462-463" />
<code_context>
+ suggestion: Optional improvement text (only present when rating < threshold).
+ """
+ user_id = interaction.user.id
+ context = self.pending_feedback_context_by_user.get(user_id)
+
+ if context is None:
+ self.log.warning("Unable to determine guild context for feedback from user %s", user_id)
+ message = "Response received, but I couldn't link it to a server context. Please try again."
+ if not interaction.response.is_done():
+ await interaction.response.send_message(message, ephemeral=True)
+ else:
+ await interaction.followup.send(message, ephemeral=True)
+ return
+
+ guild_id, event_name = context
+
+ if guild_id not in self.feedback_data:
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Feedback context is never cleared, which can cause stale associations and unnecessary growth.
After feedback is saved, the `(guild_id, event_name)` entry for this user remains in `pending_feedback_context_by_user`. This can leave stale context (e.g. if the user leaves the guild or events are renamed) and allow later interactions to be incorrectly tied to an old event. Please clear the entry once feedback is persisted (e.g. use `pop` when reading it or delete it afterward) to prevent mis-associations and unbounded growth of this mapping.
```suggestion
user_id = interaction.user.id
context = self.pending_feedback_context_by_user.pop(user_id, None)
```
</issue_to_address>
### Comment 3
<location path="capy_discord/exts/event_feedback/event_feedback.py" line_range="428-432" />
<code_context>
+ block_body = "\n\n".join(lines) if lines else "(No responses yet)"
+ full_text = f"**Feedback for {event_name}**\n\n{block_body}"
+
+ if len(full_text) <= _MAX_REPORT_CHARS:
+ await interaction.response.send_message(full_text, ephemeral=True)
+ return
+
+ await interaction.response.send_message(f"Feedback for {event_name} (part 1):", ephemeral=True)
+ current_chunk = ""
+ part = 1
</code_context>
<issue_to_address>
**suggestion:** Multipart feedback messages duplicate the "part 1" header and could be structured more cleanly.
When `full_text` exceeds `_MAX_REPORT_CHARS`, we first send `"Feedback for {event_name} (part 1):"` with no content, then send chunks labeled `"Part {part}:\n{chunk}"` starting again at part 1. This creates two different "part 1" messages, and the first has no feedback.
Consider either:
- Removing the standalone header and including it on the first chunk, e.g. `"Feedback for {event_name} (part 1):\n{chunk}"`, or
- Keeping the header but starting chunk numbering at part 2.
This would eliminate the duplicate "part 1" and make the message sequence clearer.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| user_id = interaction.user.id | ||
| context = self.pending_feedback_context_by_user.get(user_id) |
There was a problem hiding this comment.
suggestion (bug_risk): Feedback context is never cleared, which can cause stale associations and unnecessary growth.
After feedback is saved, the (guild_id, event_name) entry for this user remains in pending_feedback_context_by_user. This can leave stale context (e.g. if the user leaves the guild or events are renamed) and allow later interactions to be incorrectly tied to an old event. Please clear the entry once feedback is persisted (e.g. use pop when reading it or delete it afterward) to prevent mis-associations and unbounded growth of this mapping.
| user_id = interaction.user.id | |
| context = self.pending_feedback_context_by_user.get(user_id) | |
| user_id = interaction.user.id | |
| context = self.pending_feedback_context_by_user.pop(user_id, None) |
Scaffold for the event feedback function
Summary by Sourcery
Integrate a new event feedback system that DMs members post-event to collect ratings and optional written suggestions, and allows admins to view aggregated feedback per event.
New Features: