Skip to content

Administration Cog

This Cog (src.oscar.cogs.admin) handles internal maintenance tasks. It is protected by strict permission checks, ensuring only authorized personnel (DEVELOPER_IDS or Server Administrators) can access sensitive user feedback.

Architecture

classDiagram
    class Administration {
        +bot : Bot
        +review_feedback(interaction)
    }

    class ReviewFeedbackView {
        +periods : list
        +feedback : list
        +selected_zoom : int
        +check_user(interaction)
        -_build_content()
        -_create_boxplot_image()
        -_create_timeline_image()
        -_update(interaction)
        -__filter_feedback()
    }

    class TranslatedView {
        +language_code : LanguageCode
        +_create_language_toggle()
    }

    TranslatedView <|-- ReviewFeedbackView
    Administration ..> ReviewFeedbackView : creates

Administration Class

The entry point for admin commands. It injects the bot instance and logs the loading process.

src.oscar.cogs.admin.Administration

Administration(bot: Bot)

Bases: Cog

Class for the administration cog.

Source code in src/oscar/cogs/admin.py
def __init__(self, bot: commands.Bot):
    self.bot: commands.Bot = bot
    logger.info("Loaded Administration cog")

review_feedback async

review_feedback(interaction: Interaction)

Command to review feedback given by users.

Source code in src/oscar/cogs/admin.py
@app_commands.default_permissions(administrator=True)
@app_commands.command(description="Review the given feedback")
@app_commands.check(may_review_feedback)
async def review_feedback(self, interaction: discord.Interaction):
    """ Command to review feedback given by users."""
    # \/ important (makes the bot say it"s thinking)
    # ephemeral: the plots are built from what students wrote, the channel has no
    # business reading along
    _ = await interaction.response.defer(ephemeral=True)
    db: Database = get_database()
    feedback: list[FeedbackReviewDict] = db.get_feedback()

    boxplot_file: File = await create_feedback_boxplot(feedback)
    timeline_file: File = await create_feedback_timeline(feedback)

    await interaction.followup.send(
        view=ReviewFeedbackView(
            interaction=interaction,
            feedback=feedback,
            images=[boxplot_file, timeline_file],
        ),
        files=[boxplot_file, timeline_file],
        ephemeral=True,
    )

Module Setup

Required function for discord.py to load this extension.

src.oscar.cogs.admin.setup async

setup(bot: Bot)

Setup the Administration cog.

Source code in src/oscar/cogs/admin.py
async def setup(bot: commands.Bot):
    """ Setup the Administration cog."""
    await bot.add_cog(Administration(bot))

Feedback Review View

The [ReviewFeedbackView](../ui/feature_views.md) is an interactive UI that displays statistics and graphs. It inherits from TranslatedView to support both English and German interfaces.

Key Features:

  • Time Filtering: Users can filter feedback by periods ranging from "1 Hour" to "All Time". The view dynamically selects up to 5 evenly distributed time intervals to display as buttons.
  • Security: Interactions are protected via check_user to ensure only the command author can control the view.
  • Live Updates: The graphs (Boxplot & Timeline) are regenerated on-the-fly when filters change.
  • Data Export: Includes a download button (⤓) to export the raw feedback data as a .json file.

src.oscar.cogs.admin.ReviewFeedbackView

ReviewFeedbackView(interaction: Interaction, feedback: list[FeedbackReviewDict], images: list[File], default_language: LanguageCode = LanguageCode.EN)

Bases: TranslatedView

Feedback (review) view that extends TranslatedView for language switching.

Source code in src/oscar/cogs/admin.py
def __init__(
        self,
        interaction: discord.Interaction,
        feedback: list[FeedbackReviewDict],
        images: list[discord.File],
        default_language: LanguageCode = LanguageCode.EN
    ):
    self.interaction: Interaction[Client] = interaction
    self.feedback = feedback
    self.files = images


    # make periods unique and sorted with None at the end
    self.periods = list(set(self.periods))
    self.periods.remove(None)
    self.periods.sort()  # pyright: ignore[reportCallIssue]
    # TODO: add code to only select valid (sensible) time spans
    self.periods.append(None)

    # limit time spans to a max of 5 options
    if len(self.periods) == 0:
        self.periods = [None]
    elif len(self.periods) > 5:
        step_size: float = (len(self.periods) - 1) / 4
        indices_to_keep: list[int] = [0]
        for i in range(1, 4):
            indices_to_keep.append(int(round(i * step_size)))
        self.periods = [self.periods[i] for i in indices_to_keep]
        self.periods.append(None)

    self.__filter_feedback()

    super().__init__(default_language)

check_user async

check_user(interaction: Interaction) -> bool

Chec weather the user has the right to perform the interaction

Source code in src/oscar/cogs/admin.py
async def check_user(self, interaction: discord.Interaction) -> bool:
    """ Chec weather the user has the right to perform the interaction"""
    if interaction.user == self.interaction.user:
        return True

    _ = await interaction.response.send_message(
        "Only the original auther can interact",
        ephemeral=True
    )
    return False

_build_content

_build_content()

Build the feedback (review) view content with current language.

Source code in src/oscar/cogs/admin.py
@override
def _build_content(self):
    """ Build the feedback (review) view content with current language."""

    # time_span_buttons: list[Button[LayoutView]] = []
    time_span_buttons: ActionRow[LayoutView] = ActionRow()
    for duration in self.periods:
        button: Button[LayoutView] = Button(
            id = duration,
            label=format_duration(duration, self.language_code),
            disabled=self.selected_zoom == duration,
            style=discord.ButtonStyle.primary if self.selected_zoom == duration else
                  discord.ButtonStyle.secondary
        )

        async def time_span_callback(
            interaction: discord.Interaction,
            duration: int|None = duration
        ):
            if not await self.check_user(interaction):
                return
            if duration == self.selected_zoom:
                return

            self.selected_zoom = duration
            self.__filter_feedback()
            await self._update(interaction)

        button.callback = time_span_callback
        _ = time_span_buttons.add_item(button)


    download_button: Button[LayoutView] = Button(
        label="⤓",
        disabled=len(self.feedback)==0,
        style=discord.ButtonStyle.secondary,
    )

    async def download_callback(interaction: discord.Interaction):
        if not await self.check_user(interaction):
            return

        json_data: str = json.dumps(self.feedback, indent=4)
        json_bytes: BytesIO = io.BytesIO(json_data.encode('utf-8'))

        file: File = File(json_bytes, filename="feedback_data.json")

        _ = await interaction.response.send_message(
            FEEDBACK_REVIEW["finished_download"][self.language_code],
            file=file,
            ephemeral=True
        )

    download_button.callback = download_callback


    _ = self.add_item(
        Container(
            Section[LayoutView](
                TextDisplay(f"# {FEEDBACK_REVIEW['review_feedback'][self.language_code]}"),
                accessory=self._create_language_toggle(),
            ),
            Separator(),

            # TextDisplay(f"## {FEEDBACK_REVIEW["intuitiveness"][self.language_code]}"),
            TextDisplay(f"-# {FEEDBACK_REVIEW['zoom'][self.language_code]}"),
            time_span_buttons,
            Separator(),
            Separator(),

            MediaGallery(
                MediaGalleryItem(self.files[0]), # boxplot image
            ),
            Separator(),
            MediaGallery(
                MediaGalleryItem(self.files[1]), # timeline image
            ),
            Separator(),
            Separator(),

            # section for sending complete feedback data as a `.json` to one"s own dms
            Section[LayoutView](
                TextDisplay(
                    f"-# {FEEDBACK_REVIEW['data_download'][self.language_code]} _(`.json`)_"
                ),
                accessory=download_button,
            ),
        )
    )

_create_boxplot_image async

_create_boxplot_image() -> discord.File

Generate boxplot image from filtered feedback data.

Source code in src/oscar/cogs/admin.py
async def _create_boxplot_image(self) -> discord.File:
    """Generate boxplot image from filtered feedback data."""
    return await create_feedback_boxplot(
        self.feedback_filtered,
        self.language_code
    )

_create_timeline_image async

_create_timeline_image() -> discord.File

Generate timeline image from filtered feedback data.

Source code in src/oscar/cogs/admin.py
async def _create_timeline_image(self) -> discord.File:
    """Generate timeline image from filtered feedback data."""
    return await create_feedback_timeline(
        self.feedback,
        self.selected_zoom,
        self.language_code
    )

_update async

_update(interaction: Interaction)
Source code in src/oscar/cogs/admin.py
@override
async def _update(self, interaction: discord.Interaction):
    _ = self.clear_items()
    self._build_content()

    boxplot_file = await self._create_boxplot_image()
    timeline_file = await self._create_timeline_image()

    _ = await interaction.response.defer()
    _ = await interaction.edit_original_response(
        view=self,
        attachments=[boxplot_file, timeline_file]
    )

__filter_feedback

__filter_feedback()

Filter the feedback based on the selected zoom (time span).

Source code in src/oscar/cogs/admin.py
def __filter_feedback(self):
    """ Filter the feedback based on the selected zoom (time span)."""
    if self.selected_zoom is None:
        self.feedback_filtered = self.feedback
        return

    current_time: int = int(time.time())
    filtered: list[FeedbackReviewDict] = [
        fb for fb in self.feedback
        if (current_time - fb["timestamp"]) <= self.selected_zoom
    ]

    self.feedback_filtered = filtered

Helper Functions

src.oscar.cogs.admin.is_admin_or_developer

is_admin_or_developer(interaction: Interaction) -> bool

Check if user is a developer, or an administrator of the home server.

An administrator of any other server is a stranger, see util.operators.

Source code in src/oscar/cogs/admin.py
def is_admin_or_developer(interaction: discord.Interaction) -> bool:
    """ Check if user is a developer, or an administrator of the home server.

        An administrator of any other server is a stranger, see `util.operators`.
    """
    # a direct message has a plain user, and a plain user has no server permissions
    permissions = getattr(interaction.user, "guild_permissions", None)
    return is_operator(
        user_id=interaction.user.id,
        guild_id=interaction.guild_id,
        is_administrator=bool(permissions is not None and permissions.administrator),
    )