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
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 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 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 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),
)
|