Skip to content

Feature/capr 44 integrate post event rating feedback system#135

Open
belofe wants to merge 10 commits intodevelopfrom
feature/capr-44-integrate-post-event-rating-feedback-system
Open

Feature/capr 44 integrate post event rating feedback system#135
belofe wants to merge 10 commits intodevelopfrom
feature/capr-44-integrate-post-event-rating-feedback-system

Conversation

@belofe
Copy link
Copy Markdown

@belofe belofe commented Mar 20, 2026

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:

  • Add a post-event feedback cog with DM-based rating buttons and follow-up modal for low ratings.
  • Store collected feedback in an in-memory schema with per-guild, per-event aggregation and viewing commands.
  • Introduce a configuration option to limit feedback DMs to a single test user for development.

Ethan Beloff and others added 7 commits February 13, 2026 12:21
- 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
@belofe belofe requested a review from shamikkarkhanis March 20, 2026 21:08
@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai bot commented Mar 20, 2026

Reviewer's Guide

Adds 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 flow

sequenceDiagram
    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")
Loading

Updated class diagram for event feedback cog and related types

classDiagram
    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
Loading

File-Level Changes

Change Details Files
Introduce an event feedback system as a Discord cog with DM-based rating UI, in-memory storage, and admin reporting commands.
  • Add a test_user_id configuration option to restrict feedback DMs to a single member in test mode.
  • Implement an EventFeedback cog that manages feedback DMs, in-memory storage keyed by guild and event, and integrates with the existing DirectMessenger service.
  • Create RatingView and RatingRangeButton UI components to collect 1–10 ratings via three range buttons and branch to a follow-up modal for low scores.
  • Implement an ImprovementModal to capture optional free-text suggestions for low ratings and to finalize feedback submission.
  • Add slash commands /event_feedback and /view_feedback (with autocomplete) to send surveys and view collected feedback per event, including chunked output for long reports.
  • Define a Pydantic EventFeedbackSchema to validate rating and suggestion fields for stored feedback records.
capy_discord/config.py
capy_discord/exts/event_feedback/event_feedback.py
capy_discord/exts/event_feedback/_schemas.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 3 issues, and left some high level feedback:

  • 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.
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>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +462 to +463
user_id = interaction.user.id
context = self.pending_feedback_context_by_user.get(user_id)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Suggested change
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)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant