From c5259bc8c5c001df07d37fc0f8d26f6aa9b4ccdd Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Sat, 19 Jun 2021 13:54:54 +0200 Subject: [PATCH 01/68] Added new DB Models --- general/betheprofessional/models.py | 30 ++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/general/betheprofessional/models.py b/general/betheprofessional/models.py index ac27ec62f..88bce1ac0 100644 --- a/general/betheprofessional/models.py +++ b/general/betheprofessional/models.py @@ -1,16 +1,32 @@ -from typing import Union +from typing import Union, Optional from PyDrocsid.database import db -from sqlalchemy import Column, BigInteger +from sqlalchemy import Column, BigInteger, String, Integer, ForeignKey -class BTPRole(db.Base): - __tablename__ = "btp_role" +class BTPTopic(db.Base): + __tablename__ = "btp_topic" - role_id: Union[Column, int] = Column(BigInteger, primary_key=True, unique=True) + id: Union[Column, int] = Column(Integer, primary_key=True) + name: Union[Column, str] = Column(String) + parent: Union[Column, int] = Column(Integer) + role_id: Union[Column, int] = Column(BigInteger) @staticmethod - async def create(role_id: int) -> "BTPRole": - row = BTPRole(role_id=role_id) + async def create(name: str, role_id: int, parent: Optional[int]) -> "BTPTopic": + row = BTPTopic(name=name, role_id=role_id, parent=parent) + await db.add(row) + return row + + +class BTPUser(db.Base): + __tablename__ = "btp_users" + + user_id: Union[Column, int] = Column(BigInteger, primary_key=True) + topic: Union[Column, int] = Column(Integer, ForeignKey(BTPTopic.id)) + + @staticmethod + async def create(user_id: int, topic: int) -> "BTPUser": + row = BTPUser(user_id=user_id, topic=topic) await db.add(row) return row From f699b7f4f64d3cca678ba39791584f5aa1b98052 Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Sat, 19 Jun 2021 21:32:54 +0200 Subject: [PATCH 02/68] Updates Parameters --- general/betheprofessional/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/general/betheprofessional/models.py b/general/betheprofessional/models.py index 88bce1ac0..92f9d5dc7 100644 --- a/general/betheprofessional/models.py +++ b/general/betheprofessional/models.py @@ -13,7 +13,7 @@ class BTPTopic(db.Base): role_id: Union[Column, int] = Column(BigInteger) @staticmethod - async def create(name: str, role_id: int, parent: Optional[int]) -> "BTPTopic": + async def create(name: str, role_id: Union[int, None], parent: Optional[Union[int, None]]) -> "BTPTopic": row = BTPTopic(name=name, role_id=role_id, parent=parent) await db.add(row) return row From 654126830598e0154b4bb70e9bbec28ae9519e8a Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Sat, 19 Jun 2021 22:11:31 +0200 Subject: [PATCH 03/68] Started Refactoring BeTheProfessional --- general/betheprofessional/cog.py | 138 ++++++++++++------------------- 1 file changed, 51 insertions(+), 87 deletions(-) diff --git a/general/betheprofessional/cog.py b/general/betheprofessional/cog.py index 64ec0351c..691d7ae09 100644 --- a/general/betheprofessional/cog.py +++ b/general/betheprofessional/cog.py @@ -1,5 +1,5 @@ import string -from typing import List +from typing import List, Union from discord import Role, Guild, Member, Embed from discord.ext import commands @@ -12,7 +12,7 @@ from PyDrocsid.translations import t from PyDrocsid.util import calculate_edit_distance, check_role_assignable from .colors import Colors -from .models import BTPRole +from .models import BTPUser, BTPTopic from .permissions import BeTheProfessionalPermission from ...contributor import Contributor from ...pubsub import send_to_changelog @@ -26,6 +26,8 @@ def split_topics(topics: str) -> List[str]: async def parse_topics(guild: Guild, topics: str, author: Member) -> List[Role]: + # TODO + roles: List[Role] = [] all_topics: List[Role] = await list_topics(guild) for topic in split_topics(topics): @@ -37,7 +39,6 @@ async def parse_topics(guild: Guild, topics: str, author: Member) -> List[Role]: raise CommandError(t.youre_not_the_first_one(topic, author.mention)) else: if all_topics: - def dist(name: str) -> int: return calculate_edit_distance(name.lower(), topic.lower()) @@ -48,49 +49,11 @@ def dist(name: str) -> int: return roles -async def list_topics(guild: Guild) -> List[Role]: - roles: List[Role] = [] - async for btp_role in await db.stream(select(BTPRole)): - if (role := guild.get_role(btp_role.role_id)) is None: - await db.delete(btp_role) - else: - roles.append(role) - return roles - - -async def unregister_roles(ctx: Context, topics: str, *, delete_roles: bool): - guild: Guild = ctx.guild - roles: List[Role] = [] - btp_roles: List[BTPRole] = [] - names = split_topics(topics) - if not names: - raise UserInputError - - for topic in names: - for role in guild.roles: - if role.name.lower() == topic.lower(): - break - else: - raise CommandError(t.topic_not_registered(topic)) - if (btp_role := await db.first(select(BTPRole).filter_by(role_id=role.id))) is None: - raise CommandError(t.topic_not_registered(topic)) - - roles.append(role) - btp_roles.append(btp_role) - - for role, btp_role in zip(roles, btp_roles): - if delete_roles: - check_role_assignable(role) - await role.delete() - await db.delete(btp_role) - - embed = Embed(title=t.betheprofessional, colour=Colors.BeTheProfessional) - embed.description = t.topics_unregistered(cnt=len(roles)) - await send_to_changelog( - ctx.guild, - t.log_topics_unregistered(cnt=len(roles), topics=", ".join(f"`{r}`" for r in roles)), - ) - await send_long_embed(ctx, embed) +async def get_topics() -> List[BTPTopic]: + topics: List[BTPTopic] = [] + async for topic in await db.stream(select(BTPTopic)): + topics.append(topic) + return topics class BeTheProfessionalCog(Cog, name="Self Assignable Topic Roles"): @@ -104,7 +67,7 @@ async def list_topics(self, ctx: Context): """ embed = Embed(title=t.available_topics_header, colour=Colors.BeTheProfessional) - out = [role.name for role in await list_topics(ctx.guild)] + out = [topic.name for topic in await get_topics()] if not out: embed.colour = Colors.error embed.description = t.no_topics_registered @@ -144,20 +107,26 @@ async def unassign_topics(self, ctx: Context, *, topics: str): remove one or more topics (use * to remove all topics) """ + # TODO + member: Member = ctx.author if topics.strip() == "*": - roles: List[Role] = await list_topics(ctx.guild) + topics: List[BTPTopic] = await get_topics() else: - roles: List[Role] = await parse_topics(ctx.guild, topics, ctx.author) - roles = [r for r in roles if r in member.roles] - - for role in roles: - check_role_assignable(role) - - await member.remove_roles(*roles) + topics: List[BTPTopic] = await parse_topics(ctx.guild, topics, ctx.author) + # TODO Check if user has + for topic in topics: + user_has_topic = False + for user_topic in db.all(select(BTPUser).filter_by(user_id=member.id)): + if user_topic.id == topic.id: + user_has_topic = True + if not user_has_topic: + raise CommandError("you have da topic not") # TODO + for topic in topics: + await db.delete(topic) embed = Embed(title=t.betheprofessional, colour=Colors.BeTheProfessional) - embed.description = t.topics_removed(cnt=len(roles)) + embed.description = t.topics_removed(cnt=len(topics)) await reply(ctx, embed=embed) @commands.command(name="*") @@ -168,43 +137,29 @@ async def register_topics(self, ctx: Context, *, topics: str): register one or more new topics """ - guild: Guild = ctx.guild names = split_topics(topics) if not names: raise UserInputError valid_chars = set(string.ascii_letters + string.digits + " !#$%&'()+-./:<=>?[\\]^_`{|}~") - to_be_created: List[str] = [] - roles: List[Role] = [] + registered_topics: list[tuple[str, Union[BTPTopic, None]]] = [] for topic in names: if any(c not in valid_chars for c in topic): raise CommandError(t.topic_invalid_chars(topic)) - for role in guild.roles: - if role.name.lower() == topic.lower(): - break - else: - to_be_created.append(topic) - continue - - if await db.exists(select(BTPRole).filter_by(role_id=role.id)): + if await db.exists(select(BTPTopic).filter_by(name=topic)): raise CommandError(t.topic_already_registered(topic)) + else: + registered_topics.append((topic, None)) - check_role_assignable(role) - - roles.append(role) - - for name in to_be_created: - roles.append(await guild.create_role(name=name, mentionable=True)) - - for role in roles: - await BTPRole.create(role.id) + for registered_topic in registered_topics: + await BTPTopic.create(registered_topic[0], None, registered_topic[1]) embed = Embed(title=t.betheprofessional, colour=Colors.BeTheProfessional) - embed.description = t.topics_registered(cnt=len(roles)) + embed.description = t.topics_registered(cnt=len(registered_topics)) await send_to_changelog( ctx.guild, - t.log_topics_registered(cnt=len(roles), topics=", ".join(f"`{r}`" for r in roles)), + t.log_topics_registered(cnt=len(registered_topics), topics=", ".join(f"`{r}`" for r in registered_topics)), ) await reply(ctx, embed=embed) @@ -216,14 +171,23 @@ async def delete_topics(self, ctx: Context, *, topics: str): delete one or more topics """ - await unregister_roles(ctx, topics, delete_roles=True) + topics = split_topics(topics) - @commands.command(name="%") - @BeTheProfessionalPermission.manage.check - @guild_only() - async def unregister_topics(self, ctx: Context, *, topics: str): - """ - unregister one or more topics without deleting the roles - """ + delete_topics: list[BTPTopic] = [] + + for topic in topics: + if not await db.exists(select(BTPTopic).filter_by(name=topic)): + raise CommandError(t.topic_not_registered(topic)) + else: + delete_topics.append(await db.first(select(BTPTopic).filter_by(name=topic))) + + for topic in delete_topics: + await db.delete(topic) # TODO Delete Role - await unregister_roles(ctx, topics, delete_roles=False) + embed = Embed(title=t.betheprofessional, colour=Colors.BeTheProfessional) + embed.description = t.topics_unregistered(cnt=len(delete_topics)) + await send_to_changelog( + ctx.guild, + t.log_topics_unregistered(cnt=len(delete_topics), topics=", ".join(f"`{r}`" for r in delete_topics)), + ) + await send_long_embed(ctx, embed) From 3102fccb4c7357e28c825a963b4cbdbc6f404dd6 Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Sat, 19 Jun 2021 23:14:24 +0200 Subject: [PATCH 04/68] Fixed Model --- general/betheprofessional/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/general/betheprofessional/models.py b/general/betheprofessional/models.py index 92f9d5dc7..191eeb230 100644 --- a/general/betheprofessional/models.py +++ b/general/betheprofessional/models.py @@ -8,7 +8,7 @@ class BTPTopic(db.Base): __tablename__ = "btp_topic" id: Union[Column, int] = Column(Integer, primary_key=True) - name: Union[Column, str] = Column(String) + name: Union[Column, str] = Column(String(255)) parent: Union[Column, int] = Column(Integer) role_id: Union[Column, int] = Column(BigInteger) From 01716542630df63829220975160310cd23b70987 Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Sat, 19 Jun 2021 23:33:29 +0200 Subject: [PATCH 05/68] Fixed Model Primary Key --- general/betheprofessional/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/general/betheprofessional/models.py b/general/betheprofessional/models.py index 191eeb230..51ae31c03 100644 --- a/general/betheprofessional/models.py +++ b/general/betheprofessional/models.py @@ -22,7 +22,8 @@ async def create(name: str, role_id: Union[int, None], parent: Optional[Union[in class BTPUser(db.Base): __tablename__ = "btp_users" - user_id: Union[Column, int] = Column(BigInteger, primary_key=True) + id: Union[Column, int] = Column(Integer, primary_key=True) + user_id: Union[Column, int] = Column(BigInteger) topic: Union[Column, int] = Column(Integer, ForeignKey(BTPTopic.id)) @staticmethod From 7d8d66835bf8e8d1aba0c9c7b25bf87054f25e27 Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Sat, 19 Jun 2021 23:34:17 +0200 Subject: [PATCH 06/68] Refactored function and the cog is now runnable --- general/betheprofessional/cog.py | 49 ++++++++++++++------------------ 1 file changed, 21 insertions(+), 28 deletions(-) diff --git a/general/betheprofessional/cog.py b/general/betheprofessional/cog.py index 691d7ae09..a264fefab 100644 --- a/general/betheprofessional/cog.py +++ b/general/betheprofessional/cog.py @@ -25,28 +25,25 @@ def split_topics(topics: str) -> List[str]: return [topic for topic in map(str.strip, topics.replace(";", ",").split(",")) if topic] -async def parse_topics(guild: Guild, topics: str, author: Member) -> List[Role]: +async def parse_topics(topics_str: str, author: Member) -> List[BTPTopic]: # TODO - roles: List[Role] = [] - all_topics: List[Role] = await list_topics(guild) - for topic in split_topics(topics): - for role in guild.roles: - if role.name.lower() == topic.lower(): - if role in all_topics: - break - if not role.managed and role >= guild.me.top_role: - raise CommandError(t.youre_not_the_first_one(topic, author.mention)) - else: - if all_topics: - def dist(name: str) -> int: - return calculate_edit_distance(name.lower(), topic.lower()) - - best_match = min([r.name for r in all_topics], key=dist) + topics: List[BTPTopic] = [] + all_topics: List[BTPTopic] = await get_topics() + for topic in split_topics(topics_str): + query = select(BTPTopic).filter_by(name=topic) + topic_db = await db.first(query) + if not (await db.exists(query)): + def dist(name: str) -> int: + return calculate_edit_distance(name.lower(), topic.lower()) + + best_match = min([r.name for r in all_topics], key=dist) + if best_match: raise CommandError(t.topic_not_found_did_you_mean(topic, best_match)) - raise CommandError(t.topic_not_found(topic)) - roles.append(role) - return roles + else: + raise CommandError(t.topic_not_found(topic)) + topics.append(topic_db) + return topics async def get_topics() -> List[BTPTopic]: @@ -86,16 +83,12 @@ async def assign_topics(self, ctx: Context, *, topics: str): """ member: Member = ctx.author - roles: List[Role] = [r for r in await parse_topics(ctx.guild, topics, ctx.author) if r not in member.roles] - - for role in roles: - check_role_assignable(role) - - await member.add_roles(*roles) - + topics: List[BTPTopic] = [topic for topic in await parse_topics(topics, ctx.author)] # TODO check if user has it already + for topic in topics: + await BTPUser.create(member.id, topic.id) embed = Embed(title=t.betheprofessional, colour=Colors.BeTheProfessional) - embed.description = t.topics_added(cnt=len(roles)) - if not roles: + embed.description = t.topics_added(cnt=len(topics)) + if not topics: embed.colour = Colors.error await reply(ctx, embed=embed) From d16256a9512fb95bdd00158c312cc7ae912a4873 Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Sun, 20 Jun 2021 09:29:04 +0200 Subject: [PATCH 07/68] Added Group to DB Model --- general/betheprofessional/models.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/general/betheprofessional/models.py b/general/betheprofessional/models.py index 51ae31c03..7217e610e 100644 --- a/general/betheprofessional/models.py +++ b/general/betheprofessional/models.py @@ -11,10 +11,13 @@ class BTPTopic(db.Base): name: Union[Column, str] = Column(String(255)) parent: Union[Column, int] = Column(Integer) role_id: Union[Column, int] = Column(BigInteger) + group: Union[Column, str] = Column(String(255)) @staticmethod - async def create(name: str, role_id: Union[int, None], parent: Optional[Union[int, None]]) -> "BTPTopic": - row = BTPTopic(name=name, role_id=role_id, parent=parent) + async def create( + name: str, role_id: Union[int, None], group: str, parent: Optional[Union[int, None]] + ) -> "BTPTopic": + row = BTPTopic(name=name, role_id=role_id, parent=parent, group=group) await db.add(row) return row From 58de9d3f8c5a33831430482dc606b896189ace1f Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Sun, 20 Jun 2021 09:29:16 +0200 Subject: [PATCH 08/68] New Translation --- general/betheprofessional/translations/en.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/general/betheprofessional/translations/en.yml b/general/betheprofessional/translations/en.yml index fbfabb207..85bc88954 100644 --- a/general/betheprofessional/translations/en.yml +++ b/general/betheprofessional/translations/en.yml @@ -38,3 +38,5 @@ topics_unregistered: log_topics_unregistered: one: "The **topic** {topics} has been **removed**." many: "{cnt} **topics** have been **removed**: {topics}" + +parent_not_exists: "Parent `{}` doesn't exists" \ No newline at end of file From dd0a7d375efa1a1f6c47cae6bdbd760b43a97080 Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Sun, 20 Jun 2021 09:31:48 +0200 Subject: [PATCH 09/68] Fixed bugs, added parent parser, added parents to params --- general/betheprofessional/cog.py | 104 +++++++++++++++++++++---------- 1 file changed, 72 insertions(+), 32 deletions(-) diff --git a/general/betheprofessional/cog.py b/general/betheprofessional/cog.py index a264fefab..58db09a48 100644 --- a/general/betheprofessional/cog.py +++ b/general/betheprofessional/cog.py @@ -1,7 +1,7 @@ import string from typing import List, Union -from discord import Role, Guild, Member, Embed +from discord import Member, Embed, Role from discord.ext import commands from discord.ext.commands import guild_only, Context, CommandError, UserInputError @@ -10,7 +10,7 @@ from PyDrocsid.database import db, select from PyDrocsid.embeds import send_long_embed from PyDrocsid.translations import t -from PyDrocsid.util import calculate_edit_distance, check_role_assignable +from PyDrocsid.util import calculate_edit_distance from .colors import Colors from .models import BTPUser, BTPTopic from .permissions import BeTheProfessionalPermission @@ -25,15 +25,34 @@ def split_topics(topics: str) -> List[str]: return [topic for topic in map(str.strip, topics.replace(";", ",").split(",")) if topic] -async def parse_topics(topics_str: str, author: Member) -> List[BTPTopic]: - # TODO +async def split_parents(topics: List[str]) -> List[tuple[str, str, Union[BTPTopic, None]]]: + result: List[tuple[str, str, Union[BTPTopic, None]]] = [] + for topic in topics: + topic_tree = topic.split("/") + if len(topic_tree) > 3 or len(topic_tree) < 2: + raise UserInputError # TODO ? + group = topic_tree[0] + query = select(BTPTopic).filter_by(name=topic_tree[1]) + parent: Union[BTPTopic, None, CommandError] = ( + (await db.first(query) if await db.exists(query) else CommandError(t.parent_not_exists(topic_tree[1]))) + if len(topic_tree) > 2 + else None + ) + if isinstance(parent, CommandError): + raise parent + topic = topic_tree[-1] + result.append((topic, group, parent)) + return result + +async def parse_topics(topics_str: str) -> List[BTPTopic]: topics: List[BTPTopic] = [] all_topics: List[BTPTopic] = await get_topics() for topic in split_topics(topics_str): query = select(BTPTopic).filter_by(name=topic) topic_db = await db.first(query) - if not (await db.exists(query)): + if not (await db.exists(query)) and len(all_topics) > 0: + def dist(name: str) -> int: return calculate_edit_distance(name.lower(), topic.lower()) @@ -42,6 +61,8 @@ def dist(name: str) -> int: raise CommandError(t.topic_not_found_did_you_mean(topic, best_match)) else: raise CommandError(t.topic_not_found(topic)) + elif not (await db.exists(query)): + raise CommandError(t.no_topics_registered) topics.append(topic_db) return topics @@ -83,7 +104,12 @@ async def assign_topics(self, ctx: Context, *, topics: str): """ member: Member = ctx.author - topics: List[BTPTopic] = [topic for topic in await parse_topics(topics, ctx.author)] # TODO check if user has it already + topics: List[BTPTopic] = [ + topic + for topic in await parse_topics(topics) + if (await db.exists(select(BTPTopic).filter_by(id=topic.id))) + and not (await db.exists(select(BTPUser).filter_by(user_id=member.id, topic=topic.id))) + ] for topic in topics: await BTPUser.create(member.id, topic.id) embed = Embed(title=t.betheprofessional, colour=Colors.BeTheProfessional) @@ -99,27 +125,21 @@ async def unassign_topics(self, ctx: Context, *, topics: str): """ remove one or more topics (use * to remove all topics) """ - - # TODO - member: Member = ctx.author if topics.strip() == "*": topics: List[BTPTopic] = await get_topics() else: - topics: List[BTPTopic] = await parse_topics(ctx.guild, topics, ctx.author) - # TODO Check if user has - for topic in topics: - user_has_topic = False - for user_topic in db.all(select(BTPUser).filter_by(user_id=member.id)): - if user_topic.id == topic.id: - user_has_topic = True - if not user_has_topic: - raise CommandError("you have da topic not") # TODO + topics: List[BTPTopic] = await parse_topics(topics) + affected_topics: List[BTPTopic] = [] for topic in topics: + if await db.exists(select(BTPUser).filter_by(user_id=member.id, topic=topic.id)): + affected_topics.append(topic) + + for topic in affected_topics: await db.delete(topic) embed = Embed(title=t.betheprofessional, colour=Colors.BeTheProfessional) - embed.description = t.topics_removed(cnt=len(topics)) + embed.description = t.topics_removed(cnt=len(affected_topics)) await reply(ctx, embed=embed) @commands.command(name="*") @@ -131,28 +151,38 @@ async def register_topics(self, ctx: Context, *, topics: str): """ names = split_topics(topics) - if not names: + topics: List[tuple[str, str, Union[BTPTopic, None]]] = await split_parents(names) + if not names or not topics: raise UserInputError valid_chars = set(string.ascii_letters + string.digits + " !#$%&'()+-./:<=>?[\\]^_`{|}~") - registered_topics: list[tuple[str, Union[BTPTopic, None]]] = [] - for topic in names: - if any(c not in valid_chars for c in topic): + registered_topics: List[tuple[str, str, Union[BTPTopic, None]]] = [] + for topic in topics: + if any(c not in valid_chars for c in topic[0]): raise CommandError(t.topic_invalid_chars(topic)) - if await db.exists(select(BTPTopic).filter_by(name=topic)): - raise CommandError(t.topic_already_registered(topic)) + if await db.exists(select(BTPTopic).filter_by(name=topic[0])): + raise CommandError( + t.topic_already_registered(f"{topic[1]}/{topic[2].name + '/' if topic[2] else ''}{topic[0]}") + ) else: - registered_topics.append((topic, None)) + registered_topics.append(topic) for registered_topic in registered_topics: - await BTPTopic.create(registered_topic[0], None, registered_topic[1]) + await BTPTopic.create( + registered_topic[0], + None, + registered_topic[1], + registered_topic[2].id if registered_topic[2] is not None else None, + ) embed = Embed(title=t.betheprofessional, colour=Colors.BeTheProfessional) embed.description = t.topics_registered(cnt=len(registered_topics)) await send_to_changelog( ctx.guild, - t.log_topics_registered(cnt=len(registered_topics), topics=", ".join(f"`{r}`" for r in registered_topics)), + t.log_topics_registered( + cnt=len(registered_topics), topics=", ".join(f"`{r[0]}`" for r in registered_topics) + ), ) await reply(ctx, embed=embed) @@ -164,7 +194,7 @@ async def delete_topics(self, ctx: Context, *, topics: str): delete one or more topics """ - topics = split_topics(topics) + topics: List[str] = split_topics(topics) delete_topics: list[BTPTopic] = [] @@ -172,10 +202,20 @@ async def delete_topics(self, ctx: Context, *, topics: str): if not await db.exists(select(BTPTopic).filter_by(name=topic)): raise CommandError(t.topic_not_registered(topic)) else: - delete_topics.append(await db.first(select(BTPTopic).filter_by(name=topic))) - + btp_topic = await db.first(select(BTPTopic).filter_by(name=topic)) + delete_topics.append(btp_topic) + for child_topic in await db.all( + select(BTPTopic).filter_by(parent=btp_topic.id) + ): # TODO Recursive? Fix more level childs + delete_topics.insert(0, child_topic) for topic in delete_topics: - await db.delete(topic) # TODO Delete Role + if topic.role_id is not None: + role: Role = ctx.guild.get_role(topic.role_id) + await role.delete() + for user_topic in await db.all(select(BTPUser).filter_by(topic=topic.id)): + await db.delete(user_topic) + await db.commit() + await db.delete(topic) embed = Embed(title=t.betheprofessional, colour=Colors.BeTheProfessional) embed.description = t.topics_unregistered(cnt=len(delete_topics)) From 6c71d1403089261ac9c7bd51dfea12b331bc1baf Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Sun, 20 Jun 2021 09:44:46 +0200 Subject: [PATCH 10/68] Added help for Group/Parent/Topic formating --- general/betheprofessional/cog.py | 2 +- general/betheprofessional/translations/en.yml | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/general/betheprofessional/cog.py b/general/betheprofessional/cog.py index 58db09a48..218720266 100644 --- a/general/betheprofessional/cog.py +++ b/general/betheprofessional/cog.py @@ -30,7 +30,7 @@ async def split_parents(topics: List[str]) -> List[tuple[str, str, Union[BTPTopi for topic in topics: topic_tree = topic.split("/") if len(topic_tree) > 3 or len(topic_tree) < 2: - raise UserInputError # TODO ? + raise CommandError(t.group_parent_format_help) group = topic_tree[0] query = select(BTPTopic).filter_by(name=topic_tree[1]) parent: Union[BTPTopic, None, CommandError] = ( diff --git a/general/betheprofessional/translations/en.yml b/general/betheprofessional/translations/en.yml index 85bc88954..6000ba1ce 100644 --- a/general/betheprofessional/translations/en.yml +++ b/general/betheprofessional/translations/en.yml @@ -39,4 +39,5 @@ log_topics_unregistered: one: "The **topic** {topics} has been **removed**." many: "{cnt} **topics** have been **removed**: {topics}" -parent_not_exists: "Parent `{}` doesn't exists" \ No newline at end of file +parent_not_exists: "Parent `{}` doesn't exists" +group_parent_format_help: "Please write `Group-Name/[Parent-Name/]Topic-Name`" From 7727363297d6ebfc454ea9b86b5f2505f2547c7f Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Sun, 20 Jun 2021 09:47:21 +0200 Subject: [PATCH 11/68] Fixed trailing comma --- general/betheprofessional/cog.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/general/betheprofessional/cog.py b/general/betheprofessional/cog.py index 218720266..220cb3f17 100644 --- a/general/betheprofessional/cog.py +++ b/general/betheprofessional/cog.py @@ -181,7 +181,8 @@ async def register_topics(self, ctx: Context, *, topics: str): await send_to_changelog( ctx.guild, t.log_topics_registered( - cnt=len(registered_topics), topics=", ".join(f"`{r[0]}`" for r in registered_topics) + cnt=len(registered_topics), + topics=", ".join(f"`{r[0]}`" for r in registered_topics), ), ) await reply(ctx, embed=embed) From 50687702fc960340084b1467940472efc83d4b4f Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Sun, 20 Jun 2021 09:50:35 +0200 Subject: [PATCH 12/68] Fixed trailing commas --- general/betheprofessional/cog.py | 4 ++-- general/betheprofessional/models.py | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/general/betheprofessional/cog.py b/general/betheprofessional/cog.py index 220cb3f17..7cc3731e9 100644 --- a/general/betheprofessional/cog.py +++ b/general/betheprofessional/cog.py @@ -163,7 +163,7 @@ async def register_topics(self, ctx: Context, *, topics: str): if await db.exists(select(BTPTopic).filter_by(name=topic[0])): raise CommandError( - t.topic_already_registered(f"{topic[1]}/{topic[2].name + '/' if topic[2] else ''}{topic[0]}") + t.topic_already_registered(f"{topic[1]}/{topic[2].name + '/' if topic[2] else ''}{topic[0]}"), ) else: registered_topics.append(topic) @@ -206,7 +206,7 @@ async def delete_topics(self, ctx: Context, *, topics: str): btp_topic = await db.first(select(BTPTopic).filter_by(name=topic)) delete_topics.append(btp_topic) for child_topic in await db.all( - select(BTPTopic).filter_by(parent=btp_topic.id) + select(BTPTopic).filter_by(parent=btp_topic.id), ): # TODO Recursive? Fix more level childs delete_topics.insert(0, child_topic) for topic in delete_topics: diff --git a/general/betheprofessional/models.py b/general/betheprofessional/models.py index 7217e610e..2a8b45dad 100644 --- a/general/betheprofessional/models.py +++ b/general/betheprofessional/models.py @@ -15,7 +15,10 @@ class BTPTopic(db.Base): @staticmethod async def create( - name: str, role_id: Union[int, None], group: str, parent: Optional[Union[int, None]] + name: str, + role_id: Union[int, None], + group: str, + parent: Optional[Union[int, None]], ) -> "BTPTopic": row = BTPTopic(name=name, role_id=role_id, parent=parent, group=group) await db.add(row) From cb71c22ff0e8fd9daa738fa332eb7b7ca0e00134 Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Sun, 20 Jun 2021 10:16:46 +0200 Subject: [PATCH 13/68] Resolved PEP Problem --- general/betheprofessional/cog.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/general/betheprofessional/cog.py b/general/betheprofessional/cog.py index 7cc3731e9..a99899598 100644 --- a/general/betheprofessional/cog.py +++ b/general/betheprofessional/cog.py @@ -108,7 +108,7 @@ async def assign_topics(self, ctx: Context, *, topics: str): topic for topic in await parse_topics(topics) if (await db.exists(select(BTPTopic).filter_by(id=topic.id))) - and not (await db.exists(select(BTPUser).filter_by(user_id=member.id, topic=topic.id))) + and not (await db.exists(select(BTPUser).filter_by(user_id=member.id, topic=topic.id))) # noqa: W503 ] for topic in topics: await BTPUser.create(member.id, topic.id) @@ -206,7 +206,7 @@ async def delete_topics(self, ctx: Context, *, topics: str): btp_topic = await db.first(select(BTPTopic).filter_by(name=topic)) delete_topics.append(btp_topic) for child_topic in await db.all( - select(BTPTopic).filter_by(parent=btp_topic.id), + select(BTPTopic).filter_by(parent=btp_topic.id), ): # TODO Recursive? Fix more level childs delete_topics.insert(0, child_topic) for topic in delete_topics: From df6cda3999c16a9add611128bcbe6365d4fc6830 Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Sun, 20 Jun 2021 10:18:17 +0200 Subject: [PATCH 14/68] Reformated with black --- general/betheprofessional/cog.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/general/betheprofessional/cog.py b/general/betheprofessional/cog.py index a99899598..0591eb14c 100644 --- a/general/betheprofessional/cog.py +++ b/general/betheprofessional/cog.py @@ -108,7 +108,7 @@ async def assign_topics(self, ctx: Context, *, topics: str): topic for topic in await parse_topics(topics) if (await db.exists(select(BTPTopic).filter_by(id=topic.id))) - and not (await db.exists(select(BTPUser).filter_by(user_id=member.id, topic=topic.id))) # noqa: W503 + and not (await db.exists(select(BTPUser).filter_by(user_id=member.id, topic=topic.id))) # noqa: W503 ] for topic in topics: await BTPUser.create(member.id, topic.id) @@ -206,7 +206,7 @@ async def delete_topics(self, ctx: Context, *, topics: str): btp_topic = await db.first(select(BTPTopic).filter_by(name=topic)) delete_topics.append(btp_topic) for child_topic in await db.all( - select(BTPTopic).filter_by(parent=btp_topic.id), + select(BTPTopic).filter_by(parent=btp_topic.id), ): # TODO Recursive? Fix more level childs delete_topics.insert(0, child_topic) for topic in delete_topics: From 281eaafad950da93f77c9e6ac017e7cc70b205c2 Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Sun, 20 Jun 2021 11:45:49 +0200 Subject: [PATCH 15/68] Added group check and fixed unassing_topic command --- general/betheprofessional/cog.py | 5 ++++- general/betheprofessional/translations/en.yml | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/general/betheprofessional/cog.py b/general/betheprofessional/cog.py index 0591eb14c..b77b0cd48 100644 --- a/general/betheprofessional/cog.py +++ b/general/betheprofessional/cog.py @@ -40,6 +40,9 @@ async def split_parents(topics: List[str]) -> List[tuple[str, str, Union[BTPTopi ) if isinstance(parent, CommandError): raise parent + if parent is not None: + if group != parent.group: + raise CommandError(t.group_not_parent_group(group, parent.group)) topic = topic_tree[-1] result.append((topic, group, parent)) return result @@ -136,7 +139,7 @@ async def unassign_topics(self, ctx: Context, *, topics: str): affected_topics.append(topic) for topic in affected_topics: - await db.delete(topic) + await db.delete(await db.first(select(BTPUser).filter_by(topic=topic.id))) embed = Embed(title=t.betheprofessional, colour=Colors.BeTheProfessional) embed.description = t.topics_removed(cnt=len(affected_topics)) diff --git a/general/betheprofessional/translations/en.yml b/general/betheprofessional/translations/en.yml index 6000ba1ce..da76142f3 100644 --- a/general/betheprofessional/translations/en.yml +++ b/general/betheprofessional/translations/en.yml @@ -41,3 +41,4 @@ log_topics_unregistered: parent_not_exists: "Parent `{}` doesn't exists" group_parent_format_help: "Please write `Group-Name/[Parent-Name/]Topic-Name`" +group_not_parent_group: "The group `{}` is not the same as the group of the Parent `{}`" From bbebc70fe7e85b84bb2b4567014fd234b1346199 Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Sun, 20 Jun 2021 15:48:29 +0200 Subject: [PATCH 16/68] Improved List Feature (not ready with many bugs) --- general/betheprofessional/cog.py | 47 +++++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/general/betheprofessional/cog.py b/general/betheprofessional/cog.py index b77b0cd48..3b1296ccf 100644 --- a/general/betheprofessional/cog.py +++ b/general/betheprofessional/cog.py @@ -1,5 +1,5 @@ import string -from typing import List, Union +from typing import List, Union, Optional, Dict from discord import Member, Embed, Role from discord.ext import commands @@ -82,21 +82,54 @@ class BeTheProfessionalCog(Cog, name="Self Assignable Topic Roles"): @commands.command(name="?") @guild_only() - async def list_topics(self, ctx: Context): + async def list_topics(self, ctx: Context, parent_topic: Optional[str]): """ - list all registered topics + list all registered topics TODO """ - + parent: Union[None, BTPTopic, CommandError] = ( + None + if parent_topic is None + else await db.first(select(BTPTopic).filter_by(name=parent_topic)) + or CommandError(t.topic_not_found(parent_topic)) # noqa: W503 + ) + if isinstance(parent, CommandError): + raise parent embed = Embed(title=t.available_topics_header, colour=Colors.BeTheProfessional) - out = [topic.name for topic in await get_topics()] + grouped_topics: Dict[str, List[str]] = {} + out: List[BTPTopic] = [ + topic + for topic in await db.all(select(BTPTopic).filter_by(parent=parent if parent is None else parent.id)) + if topic.group is not None + ] if not out: embed.colour = Colors.error embed.description = t.no_topics_registered await reply(ctx, embed=embed) return - out.sort(key=str.lower) - embed.description = ", ".join(f"`{topic}`" for topic in out) + out.sort(key=lambda topic: topic.name.lower()) + for topic in out: + if topic.group.title() not in grouped_topics.keys(): + grouped_topics[topic.group] = [f"{topic.name}"] + else: + grouped_topics[topic.group.title()].append(f"{topic.name}") + + for group in grouped_topics.keys(): + embed.add_field( + name=group.title(), + value=", ".join( + [ + f"`{topic.name}" + + ( + f" ({c})`" + if (c := await db.count(select(BTPTopic).filter_by(parent=topic.id, group=topic.group))) > 0 + else "`" + ) + for topic in out + ] + ), + inline=False, + ) await send_long_embed(ctx, embed) @commands.command(name="+") From 5f9e129e5dfeef186ed50d3c97e259b0c5a10363 Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Sun, 20 Jun 2021 15:56:16 +0200 Subject: [PATCH 17/68] Fixed Topic Check command --- general/betheprofessional/cog.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/general/betheprofessional/cog.py b/general/betheprofessional/cog.py index 3b1296ccf..6802d50ab 100644 --- a/general/betheprofessional/cog.py +++ b/general/betheprofessional/cog.py @@ -197,7 +197,9 @@ async def register_topics(self, ctx: Context, *, topics: str): if any(c not in valid_chars for c in topic[0]): raise CommandError(t.topic_invalid_chars(topic)) - if await db.exists(select(BTPTopic).filter_by(name=topic[0])): + if await db.exists( + select(BTPTopic).filter_by(name=topic[0], parent=topic[2].id if topic[2] is not None else None, group=topic[1]), + ): raise CommandError( t.topic_already_registered(f"{topic[1]}/{topic[2].name + '/' if topic[2] else ''}{topic[0]}"), ) From fe70c36c0e2b4ef336c578a6d573a7cef8572ca8 Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Fri, 25 Jun 2021 18:34:26 +0200 Subject: [PATCH 18/68] New DB Model --- general/betheprofessional/models.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/general/betheprofessional/models.py b/general/betheprofessional/models.py index 2a8b45dad..d0616fd03 100644 --- a/general/betheprofessional/models.py +++ b/general/betheprofessional/models.py @@ -1,7 +1,7 @@ from typing import Union, Optional from PyDrocsid.database import db -from sqlalchemy import Column, BigInteger, String, Integer, ForeignKey +from sqlalchemy import Column, BigInteger, String, Integer, ForeignKey, Boolean class BTPTopic(db.Base): @@ -11,16 +11,16 @@ class BTPTopic(db.Base): name: Union[Column, str] = Column(String(255)) parent: Union[Column, int] = Column(Integer) role_id: Union[Column, int] = Column(BigInteger) - group: Union[Column, str] = Column(String(255)) + assignable: Union[Column, bool] = Column(Boolean) @staticmethod async def create( name: str, role_id: Union[int, None], - group: str, + assignable: bool, parent: Optional[Union[int, None]], ) -> "BTPTopic": - row = BTPTopic(name=name, role_id=role_id, parent=parent, group=group) + row = BTPTopic(name=name, role_id=role_id, parent=parent, assignable=assignable) await db.add(row) return row From 49e95113553f585e256bc463a432704aea2cc0ee Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Fri, 25 Jun 2021 18:35:30 +0200 Subject: [PATCH 19/68] New List Topics View, removed Group from DB and fixed Register Topic Command --- general/betheprofessional/cog.py | 88 +++++++++---------- general/betheprofessional/translations/en.yml | 2 +- 2 files changed, 45 insertions(+), 45 deletions(-) diff --git a/general/betheprofessional/cog.py b/general/betheprofessional/cog.py index 6802d50ab..a1542fdfb 100644 --- a/general/betheprofessional/cog.py +++ b/general/betheprofessional/cog.py @@ -25,26 +25,25 @@ def split_topics(topics: str) -> List[str]: return [topic for topic in map(str.strip, topics.replace(";", ",").split(",")) if topic] -async def split_parents(topics: List[str]) -> List[tuple[str, str, Union[BTPTopic, None]]]: - result: List[tuple[str, str, Union[BTPTopic, None]]] = [] +async def split_parents(topics: List[str], assignable: bool) -> List[tuple[str, bool, Optional[list[BTPTopic]]]]: + result: List[tuple[str, bool, Optional[list[BTPTopic]]]] = [] for topic in topics: topic_tree = topic.split("/") - if len(topic_tree) > 3 or len(topic_tree) < 2: - raise CommandError(t.group_parent_format_help) - group = topic_tree[0] - query = select(BTPTopic).filter_by(name=topic_tree[1]) - parent: Union[BTPTopic, None, CommandError] = ( - (await db.first(query) if await db.exists(query) else CommandError(t.parent_not_exists(topic_tree[1]))) - if len(topic_tree) > 2 - else None - ) - if isinstance(parent, CommandError): - raise parent - if parent is not None: - if group != parent.group: - raise CommandError(t.group_not_parent_group(group, parent.group)) + + parents: List[Union[BTPTopic, None, CommandError]] = [ + await db.first(select(BTPTopic).filter_by(name=topic)) + if await db.exists(select(BTPTopic).filter_by(name=topic)) + else CommandError(t.parent_not_exists(topic)) + for topic in topic_tree[:-1] + ] + + parents = [parent for parent in parents if parent is not None] + for parent in parents: + if isinstance(parent, CommandError): + raise parent + topic = topic_tree[-1] - result.append((topic, group, parent)) + result.append((topic, assignable, parents)) return result @@ -84,48 +83,49 @@ class BeTheProfessionalCog(Cog, name="Self Assignable Topic Roles"): @guild_only() async def list_topics(self, ctx: Context, parent_topic: Optional[str]): """ - list all registered topics TODO + list all registered topics """ - parent: Union[None, BTPTopic, CommandError] = ( + parent: Union[BTPTopic, None, CommandError] = ( None if parent_topic is None else await db.first(select(BTPTopic).filter_by(name=parent_topic)) - or CommandError(t.topic_not_found(parent_topic)) # noqa: W503 + or CommandError(t.topic_not_found(parent_topic)) # noqa: W503 ) if isinstance(parent, CommandError): raise parent + embed = Embed(title=t.available_topics_header, colour=Colors.BeTheProfessional) - grouped_topics: Dict[str, List[str]] = {} - out: List[BTPTopic] = [ - topic - for topic in await db.all(select(BTPTopic).filter_by(parent=parent if parent is None else parent.id)) - if topic.group is not None + sorted_topics: Dict[str, List[str]] = {} + topics: List[BTPTopic] = [ + topic for topic in await db.all(select(BTPTopic).filter_by(parent=None if parent is None else parent.id)) ] - if not out: + if not topics: embed.colour = Colors.error embed.description = t.no_topics_registered await reply(ctx, embed=embed) return - out.sort(key=lambda topic: topic.name.lower()) - for topic in out: - if topic.group.title() not in grouped_topics.keys(): - grouped_topics[topic.group] = [f"{topic.name}"] + topics.sort(key=lambda topic: topic.name.lower()) + root_topic: Union[BTPTopic, None] = None if parent_topic is None else await db.first( + select(BTPTopic).filter_by(name=parent_topic)) + for topic in topics: + if (root_topic.name if root_topic is not None else "Topics") not in sorted_topics.keys(): + sorted_topics[root_topic.name if root_topic is not None else "Topics"] = [f"{topic.name}"] else: - grouped_topics[topic.group.title()].append(f"{topic.name}") + sorted_topics[root_topic.name if root_topic is not None else "Topics"].append(f"{topic.name}") - for group in grouped_topics.keys(): + for root_topic in sorted_topics.keys(): embed.add_field( - name=group.title(), + name=root_topic.title(), value=", ".join( [ f"`{topic.name}" + ( f" ({c})`" - if (c := await db.count(select(BTPTopic).filter_by(parent=topic.id, group=topic.group))) > 0 + if (c := await db.count(select(BTPTopic).filter_by(parent=topic.id))) > 0 else "`" ) - for topic in out + for topic in topics ] ), inline=False, @@ -144,7 +144,7 @@ async def assign_topics(self, ctx: Context, *, topics: str): topic for topic in await parse_topics(topics) if (await db.exists(select(BTPTopic).filter_by(id=topic.id))) - and not (await db.exists(select(BTPUser).filter_by(user_id=member.id, topic=topic.id))) # noqa: W503 + and not (await db.exists(select(BTPUser).filter_by(user_id=member.id, topic=topic.id))) # noqa: W503 ] for topic in topics: await BTPUser.create(member.id, topic.id) @@ -181,27 +181,27 @@ async def unassign_topics(self, ctx: Context, *, topics: str): @commands.command(name="*") @BeTheProfessionalPermission.manage.check @guild_only() - async def register_topics(self, ctx: Context, *, topics: str): + async def register_topics(self, ctx: Context, *, topics: str, assignable: bool = True): """ register one or more new topics """ names = split_topics(topics) - topics: List[tuple[str, str, Union[BTPTopic, None]]] = await split_parents(names) + topics: List[tuple[str, bool, Optional[list[BTPTopic]]]] = await split_parents(names, assignable) if not names or not topics: raise UserInputError valid_chars = set(string.ascii_letters + string.digits + " !#$%&'()+-./:<=>?[\\]^_`{|}~") - registered_topics: List[tuple[str, str, Union[BTPTopic, None]]] = [] + registered_topics: List[tuple[str, bool, Optional[list[BTPTopic]]]] = [] for topic in topics: if any(c not in valid_chars for c in topic[0]): raise CommandError(t.topic_invalid_chars(topic)) if await db.exists( - select(BTPTopic).filter_by(name=topic[0], parent=topic[2].id if topic[2] is not None else None, group=topic[1]), + select(BTPTopic).filter_by(name=topic[0], parent=topic[2][-1].id if len(topic[2]) > 0 else None), ): raise CommandError( - t.topic_already_registered(f"{topic[1]}/{topic[2].name + '/' if topic[2] else ''}{topic[0]}"), + t.topic_already_registered(f"{topic[1]}/{topic[2][-1].name + '/' if topic[1] else ''}{topic[0]}"), ) else: registered_topics.append(topic) @@ -210,8 +210,8 @@ async def register_topics(self, ctx: Context, *, topics: str): await BTPTopic.create( registered_topic[0], None, - registered_topic[1], - registered_topic[2].id if registered_topic[2] is not None else None, + True, + registered_topic[2][-1].id if len(registered_topic[2]) > 0 else None, ) embed = Embed(title=t.betheprofessional, colour=Colors.BeTheProfessional) @@ -244,7 +244,7 @@ async def delete_topics(self, ctx: Context, *, topics: str): btp_topic = await db.first(select(BTPTopic).filter_by(name=topic)) delete_topics.append(btp_topic) for child_topic in await db.all( - select(BTPTopic).filter_by(parent=btp_topic.id), + select(BTPTopic).filter_by(parent=btp_topic.id), ): # TODO Recursive? Fix more level childs delete_topics.insert(0, child_topic) for topic in delete_topics: diff --git a/general/betheprofessional/translations/en.yml b/general/betheprofessional/translations/en.yml index da76142f3..836d54aa9 100644 --- a/general/betheprofessional/translations/en.yml +++ b/general/betheprofessional/translations/en.yml @@ -40,5 +40,5 @@ log_topics_unregistered: many: "{cnt} **topics** have been **removed**: {topics}" parent_not_exists: "Parent `{}` doesn't exists" -group_parent_format_help: "Please write `Group-Name/[Parent-Name/]Topic-Name`" +parent_format_help: "Please write `[Parents/]Topic-Name`" group_not_parent_group: "The group `{}` is not the same as the group of the Parent `{}`" From 46a02af5f136c8fc56543b584f446390ae38eccb Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Fri, 25 Jun 2021 18:42:47 +0200 Subject: [PATCH 20/68] Fixed PEP --- general/betheprofessional/cog.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/general/betheprofessional/cog.py b/general/betheprofessional/cog.py index a1542fdfb..b1e3941a1 100644 --- a/general/betheprofessional/cog.py +++ b/general/betheprofessional/cog.py @@ -89,16 +89,14 @@ async def list_topics(self, ctx: Context, parent_topic: Optional[str]): None if parent_topic is None else await db.first(select(BTPTopic).filter_by(name=parent_topic)) - or CommandError(t.topic_not_found(parent_topic)) # noqa: W503 + or CommandError(t.topic_not_found(parent_topic)) # noqa: W503 ) if isinstance(parent, CommandError): raise parent embed = Embed(title=t.available_topics_header, colour=Colors.BeTheProfessional) sorted_topics: Dict[str, List[str]] = {} - topics: List[BTPTopic] = [ - topic for topic in await db.all(select(BTPTopic).filter_by(parent=None if parent is None else parent.id)) - ] + topics: List[BTPTopic] = await db.all(select(BTPTopic).filter_by(parent=None if parent is None else parent.id)) if not topics: embed.colour = Colors.error embed.description = t.no_topics_registered @@ -119,8 +117,7 @@ async def list_topics(self, ctx: Context, parent_topic: Optional[str]): name=root_topic.title(), value=", ".join( [ - f"`{topic.name}" - + ( + f"`{topic.name}" + ( f" ({c})`" if (c := await db.count(select(BTPTopic).filter_by(parent=topic.id))) > 0 else "`" From 2abcc718b27923502c0c311cfdc88058c0879fdb Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Fri, 25 Jun 2021 18:44:24 +0200 Subject: [PATCH 21/68] Reformated with black --- general/betheprofessional/cog.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/general/betheprofessional/cog.py b/general/betheprofessional/cog.py index b1e3941a1..bda6b0d81 100644 --- a/general/betheprofessional/cog.py +++ b/general/betheprofessional/cog.py @@ -104,8 +104,9 @@ async def list_topics(self, ctx: Context, parent_topic: Optional[str]): return topics.sort(key=lambda topic: topic.name.lower()) - root_topic: Union[BTPTopic, None] = None if parent_topic is None else await db.first( - select(BTPTopic).filter_by(name=parent_topic)) + root_topic: Union[BTPTopic, None] = ( + None if parent_topic is None else await db.first(select(BTPTopic).filter_by(name=parent_topic)) + ) for topic in topics: if (root_topic.name if root_topic is not None else "Topics") not in sorted_topics.keys(): sorted_topics[root_topic.name if root_topic is not None else "Topics"] = [f"{topic.name}"] @@ -117,7 +118,8 @@ async def list_topics(self, ctx: Context, parent_topic: Optional[str]): name=root_topic.title(), value=", ".join( [ - f"`{topic.name}" + ( + f"`{topic.name}" + + ( f" ({c})`" if (c := await db.count(select(BTPTopic).filter_by(parent=topic.id))) > 0 else "`" @@ -141,7 +143,7 @@ async def assign_topics(self, ctx: Context, *, topics: str): topic for topic in await parse_topics(topics) if (await db.exists(select(BTPTopic).filter_by(id=topic.id))) - and not (await db.exists(select(BTPUser).filter_by(user_id=member.id, topic=topic.id))) # noqa: W503 + and not (await db.exists(select(BTPUser).filter_by(user_id=member.id, topic=topic.id))) # noqa: W503 ] for topic in topics: await BTPUser.create(member.id, topic.id) @@ -195,7 +197,7 @@ async def register_topics(self, ctx: Context, *, topics: str, assignable: bool = raise CommandError(t.topic_invalid_chars(topic)) if await db.exists( - select(BTPTopic).filter_by(name=topic[0], parent=topic[2][-1].id if len(topic[2]) > 0 else None), + select(BTPTopic).filter_by(name=topic[0], parent=topic[2][-1].id if len(topic[2]) > 0 else None), ): raise CommandError( t.topic_already_registered(f"{topic[1]}/{topic[2][-1].name + '/' if topic[1] else ''}{topic[0]}"), @@ -241,7 +243,7 @@ async def delete_topics(self, ctx: Context, *, topics: str): btp_topic = await db.first(select(BTPTopic).filter_by(name=topic)) delete_topics.append(btp_topic) for child_topic in await db.all( - select(BTPTopic).filter_by(parent=btp_topic.id), + select(BTPTopic).filter_by(parent=btp_topic.id), ): # TODO Recursive? Fix more level childs delete_topics.insert(0, child_topic) for topic in delete_topics: From 9f1d72ba586dfaa616d6ccaa563f9260ef3c71f4 Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Fri, 25 Jun 2021 18:51:25 +0200 Subject: [PATCH 22/68] Fixed PEP8 --- general/betheprofessional/cog.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/general/betheprofessional/cog.py b/general/betheprofessional/cog.py index bda6b0d81..71b1e3e4b 100644 --- a/general/betheprofessional/cog.py +++ b/general/betheprofessional/cog.py @@ -103,7 +103,7 @@ async def list_topics(self, ctx: Context, parent_topic: Optional[str]): await reply(ctx, embed=embed) return - topics.sort(key=lambda topic: topic.name.lower()) + topics.sort(key=lambda btp_topic: btp_topic.name.lower()) root_topic: Union[BTPTopic, None] = ( None if parent_topic is None else await db.first(select(BTPTopic).filter_by(name=parent_topic)) ) @@ -119,13 +119,13 @@ async def list_topics(self, ctx: Context, parent_topic: Optional[str]): value=", ".join( [ f"`{topic.name}" - + ( + + ( # noqa: W503 f" ({c})`" if (c := await db.count(select(BTPTopic).filter_by(parent=topic.id))) > 0 else "`" ) for topic in topics - ] + ], ), inline=False, ) From 171f720ff1705f3fc4ff447c36413f882a2cdc76 Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Fri, 25 Jun 2021 19:31:21 +0200 Subject: [PATCH 23/68] Added new topic ping command --- general/betheprofessional/cog.py | 27 ++++++++++++++++--- general/betheprofessional/translations/en.yml | 2 ++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/general/betheprofessional/cog.py b/general/betheprofessional/cog.py index 71b1e3e4b..d68ba6f7b 100644 --- a/general/betheprofessional/cog.py +++ b/general/betheprofessional/cog.py @@ -1,7 +1,7 @@ import string from typing import List, Union, Optional, Dict -from discord import Member, Embed, Role +from discord import Member, Embed, Role, Message from discord.ext import commands from discord.ext.commands import guild_only, Context, CommandError, UserInputError @@ -197,7 +197,7 @@ async def register_topics(self, ctx: Context, *, topics: str, assignable: bool = raise CommandError(t.topic_invalid_chars(topic)) if await db.exists( - select(BTPTopic).filter_by(name=topic[0], parent=topic[2][-1].id if len(topic[2]) > 0 else None), + select(BTPTopic).filter_by(name=topic[0], parent=topic[2][-1].id if len(topic[2]) > 0 else None), ): raise CommandError( t.topic_already_registered(f"{topic[1]}/{topic[2][-1].name + '/' if topic[1] else ''}{topic[0]}"), @@ -243,7 +243,7 @@ async def delete_topics(self, ctx: Context, *, topics: str): btp_topic = await db.first(select(BTPTopic).filter_by(name=topic)) delete_topics.append(btp_topic) for child_topic in await db.all( - select(BTPTopic).filter_by(parent=btp_topic.id), + select(BTPTopic).filter_by(parent=btp_topic.id), ): # TODO Recursive? Fix more level childs delete_topics.insert(0, child_topic) for topic in delete_topics: @@ -262,3 +262,24 @@ async def delete_topics(self, ctx: Context, *, topics: str): t.log_topics_unregistered(cnt=len(delete_topics), topics=", ".join(f"`{r}`" for r in delete_topics)), ) await send_long_embed(ctx, embed) + + @commands.command() + @guild_only() + async def topic(self, ctx: Context, topic_name: str, message: Optional[Message]): + topic: BTPTopic = await db.first(select(BTPTopic).filter_by(name=topic_name)) + mention: str + if topic is None: + raise CommandError(t.topic_not_found(topic_name)) + if topic.role_id is not None: + mention = ctx.guild.get_role(topic.role_id).mention + else: + topic_members: List[BTPUser] = await db.all(select(BTPUser).filter_by(topic=topic.id)) + members: List[Member] = [ctx.guild.get_member(member.user_id) for member in topic_members] + mention = ', '.join(map(lambda m: m.mention, members)) + + if mention == '': + raise CommandError(t.nobody_has_topic(topic_name)) + if message is None: + await ctx.send(mention) + else: + await message.reply(mention) diff --git a/general/betheprofessional/translations/en.yml b/general/betheprofessional/translations/en.yml index 836d54aa9..f60843273 100644 --- a/general/betheprofessional/translations/en.yml +++ b/general/betheprofessional/translations/en.yml @@ -42,3 +42,5 @@ log_topics_unregistered: parent_not_exists: "Parent `{}` doesn't exists" parent_format_help: "Please write `[Parents/]Topic-Name`" group_not_parent_group: "The group `{}` is not the same as the group of the Parent `{}`" +nobody_has_topic: "Nobody has the Topic `{}`" + From eb34e26b882273540bf871a679b573cb699b2a99 Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Fri, 25 Jun 2021 22:07:16 +0200 Subject: [PATCH 24/68] Added Role Update and fixed logger --- general/betheprofessional/cog.py | 39 ++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/general/betheprofessional/cog.py b/general/betheprofessional/cog.py index d68ba6f7b..5306b641d 100644 --- a/general/betheprofessional/cog.py +++ b/general/betheprofessional/cog.py @@ -1,13 +1,15 @@ +import logging import string +from collections import Counter from typing import List, Union, Optional, Dict -from discord import Member, Embed, Role, Message -from discord.ext import commands +from discord import Member, Embed, Role, Message, Guild +from discord.ext import commands, tasks from discord.ext.commands import guild_only, Context, CommandError, UserInputError from PyDrocsid.cog import Cog from PyDrocsid.command import reply -from PyDrocsid.database import db, select +from PyDrocsid.database import db, select, db_wrapper from PyDrocsid.embeds import send_long_embed from PyDrocsid.translations import t from PyDrocsid.util import calculate_edit_distance @@ -16,10 +18,13 @@ from .permissions import BeTheProfessionalPermission from ...contributor import Contributor from ...pubsub import send_to_changelog +from PyDrocsid.logger import get_logger tg = t.g t = t.betheprofessional +logger = get_logger(__name__) + def split_topics(topics: str) -> List[str]: return [topic for topic in map(str.strip, topics.replace(";", ",").split(",")) if topic] @@ -79,6 +84,9 @@ async def get_topics() -> List[BTPTopic]: class BeTheProfessionalCog(Cog, name="Self Assignable Topic Roles"): CONTRIBUTORS = [Contributor.Defelo, Contributor.wolflu, Contributor.MaxiHuHe04, Contributor.AdriBloober] + async def on_ready(self): + self.update_roles.start() + @commands.command(name="?") @guild_only() async def list_topics(self, ctx: Context, parent_topic: Optional[str]): @@ -89,7 +97,7 @@ async def list_topics(self, ctx: Context, parent_topic: Optional[str]): None if parent_topic is None else await db.first(select(BTPTopic).filter_by(name=parent_topic)) - or CommandError(t.topic_not_found(parent_topic)) # noqa: W503 + or CommandError(t.topic_not_found(parent_topic)) # noqa: W503 ) if isinstance(parent, CommandError): raise parent @@ -143,7 +151,7 @@ async def assign_topics(self, ctx: Context, *, topics: str): topic for topic in await parse_topics(topics) if (await db.exists(select(BTPTopic).filter_by(id=topic.id))) - and not (await db.exists(select(BTPUser).filter_by(user_id=member.id, topic=topic.id))) # noqa: W503 + and not (await db.exists(select(BTPUser).filter_by(user_id=member.id, topic=topic.id))) # noqa: W503 ] for topic in topics: await BTPUser.create(member.id, topic.id) @@ -283,3 +291,24 @@ async def topic(self, ctx: Context, topic_name: str, message: Optional[Message]) await ctx.send(mention) else: await message.reply(mention) + + @tasks.loop(seconds=30) # SET hours to 24 in Prod + @db_wrapper + # TODO Change to Config + async def update_roles(self): + logger.info('Started Update Role Loop') + topic_count: List[int] = [] + for topic in await db.all(select(BTPTopic)): + for _ in range(await db.count(select(BTPUser).filter_by(topic=topic.id))): + topic_count.append(topic.id) + topic_count: Counter = Counter(topic_count) + top_topics: List[int] = [] + for topic_count in sorted(topic_count)[:(100 if len(topic_count) >= 100 else len(topic_count))]: + top_topics.append(topic_count) + for topic in await db.all(select(BTPTopic).filter(BTPTopic.role_id != None)): # noqa: E711 + if topic.id not in top_topics: + await self.bot.guilds[0].get_role(topic.role_id).delete() + for top_topic in top_topics: + if (topic := await db.first(select(BTPTopic).filter(BTPTopic.id == top_topic, BTPTopic.role_id == None))) is not None: # noqa: E711 + topic.role_id = (await self.bot.guilds[0].create_role(name=topic.name)).id + logger.info('Created Top Topic Roles') From 79ce872673b3e84a80725eef728247cc2afadc82 Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Fri, 25 Jun 2021 22:10:41 +0200 Subject: [PATCH 25/68] Refactored with black --- general/betheprofessional/cog.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/general/betheprofessional/cog.py b/general/betheprofessional/cog.py index 5306b641d..84453ece7 100644 --- a/general/betheprofessional/cog.py +++ b/general/betheprofessional/cog.py @@ -97,7 +97,7 @@ async def list_topics(self, ctx: Context, parent_topic: Optional[str]): None if parent_topic is None else await db.first(select(BTPTopic).filter_by(name=parent_topic)) - or CommandError(t.topic_not_found(parent_topic)) # noqa: W503 + or CommandError(t.topic_not_found(parent_topic)) # noqa: W503 ) if isinstance(parent, CommandError): raise parent @@ -151,7 +151,7 @@ async def assign_topics(self, ctx: Context, *, topics: str): topic for topic in await parse_topics(topics) if (await db.exists(select(BTPTopic).filter_by(id=topic.id))) - and not (await db.exists(select(BTPUser).filter_by(user_id=member.id, topic=topic.id))) # noqa: W503 + and not (await db.exists(select(BTPUser).filter_by(user_id=member.id, topic=topic.id))) # noqa: W503 ] for topic in topics: await BTPUser.create(member.id, topic.id) @@ -205,7 +205,7 @@ async def register_topics(self, ctx: Context, *, topics: str, assignable: bool = raise CommandError(t.topic_invalid_chars(topic)) if await db.exists( - select(BTPTopic).filter_by(name=topic[0], parent=topic[2][-1].id if len(topic[2]) > 0 else None), + select(BTPTopic).filter_by(name=topic[0], parent=topic[2][-1].id if len(topic[2]) > 0 else None), ): raise CommandError( t.topic_already_registered(f"{topic[1]}/{topic[2][-1].name + '/' if topic[1] else ''}{topic[0]}"), @@ -251,7 +251,7 @@ async def delete_topics(self, ctx: Context, *, topics: str): btp_topic = await db.first(select(BTPTopic).filter_by(name=topic)) delete_topics.append(btp_topic) for child_topic in await db.all( - select(BTPTopic).filter_by(parent=btp_topic.id), + select(BTPTopic).filter_by(parent=btp_topic.id), ): # TODO Recursive? Fix more level childs delete_topics.insert(0, child_topic) for topic in delete_topics: @@ -283,9 +283,9 @@ async def topic(self, ctx: Context, topic_name: str, message: Optional[Message]) else: topic_members: List[BTPUser] = await db.all(select(BTPUser).filter_by(topic=topic.id)) members: List[Member] = [ctx.guild.get_member(member.user_id) for member in topic_members] - mention = ', '.join(map(lambda m: m.mention, members)) + mention = ", ".join(map(lambda m: m.mention, members)) - if mention == '': + if mention == "": raise CommandError(t.nobody_has_topic(topic_name)) if message is None: await ctx.send(mention) @@ -296,19 +296,21 @@ async def topic(self, ctx: Context, topic_name: str, message: Optional[Message]) @db_wrapper # TODO Change to Config async def update_roles(self): - logger.info('Started Update Role Loop') + logger.info("Started Update Role Loop") topic_count: List[int] = [] for topic in await db.all(select(BTPTopic)): for _ in range(await db.count(select(BTPUser).filter_by(topic=topic.id))): topic_count.append(topic.id) topic_count: Counter = Counter(topic_count) top_topics: List[int] = [] - for topic_count in sorted(topic_count)[:(100 if len(topic_count) >= 100 else len(topic_count))]: + for topic_count in sorted(topic_count)[: (100 if len(topic_count) >= 100 else len(topic_count))]: top_topics.append(topic_count) for topic in await db.all(select(BTPTopic).filter(BTPTopic.role_id != None)): # noqa: E711 if topic.id not in top_topics: await self.bot.guilds[0].get_role(topic.role_id).delete() for top_topic in top_topics: - if (topic := await db.first(select(BTPTopic).filter(BTPTopic.id == top_topic, BTPTopic.role_id == None))) is not None: # noqa: E711 + if ( + topic := await db.first(select(BTPTopic).filter(BTPTopic.id == top_topic, BTPTopic.role_id == None)) + ) is not None: # noqa: E711 topic.role_id = (await self.bot.guilds[0].create_role(name=topic.name)).id - logger.info('Created Top Topic Roles') + logger.info("Created Top Topic Roles") From d9d997a7f0cd08bf5efc422948ebc519ddf48507 Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Fri, 25 Jun 2021 22:13:35 +0200 Subject: [PATCH 26/68] Fixed Formating --- general/betheprofessional/cog.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/general/betheprofessional/cog.py b/general/betheprofessional/cog.py index 84453ece7..95d44baff 100644 --- a/general/betheprofessional/cog.py +++ b/general/betheprofessional/cog.py @@ -310,7 +310,7 @@ async def update_roles(self): await self.bot.guilds[0].get_role(topic.role_id).delete() for top_topic in top_topics: if ( - topic := await db.first(select(BTPTopic).filter(BTPTopic.id == top_topic, BTPTopic.role_id == None)) - ) is not None: # noqa: E711 + topic := await db.first(select(BTPTopic).filter(BTPTopic.id == top_topic, BTPTopic.role_id == None)) # noqa: E711 + ) is not None: topic.role_id = (await self.bot.guilds[0].create_role(name=topic.name)).id logger.info("Created Top Topic Roles") From 54abdcf80cbca5aee7b1ae97022b297e0c8bdbc0 Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Fri, 25 Jun 2021 22:15:47 +0200 Subject: [PATCH 27/68] Fixed Formating --- general/betheprofessional/cog.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/general/betheprofessional/cog.py b/general/betheprofessional/cog.py index 95d44baff..6ba17e4ed 100644 --- a/general/betheprofessional/cog.py +++ b/general/betheprofessional/cog.py @@ -309,8 +309,6 @@ async def update_roles(self): if topic.id not in top_topics: await self.bot.guilds[0].get_role(topic.role_id).delete() for top_topic in top_topics: - if ( - topic := await db.first(select(BTPTopic).filter(BTPTopic.id == top_topic, BTPTopic.role_id == None)) # noqa: E711 - ) is not None: + if (topic := await db.first(select(BTPTopic).filter_by(id=top_topic, role_id=None))) is not None: topic.role_id = (await self.bot.guilds[0].create_role(name=topic.name)).id logger.info("Created Top Topic Roles") From ad768eff8745d13164de86b624542605f72b2d3e Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Fri, 25 Jun 2021 22:43:20 +0200 Subject: [PATCH 28/68] Optimised Import --- general/betheprofessional/cog.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/general/betheprofessional/cog.py b/general/betheprofessional/cog.py index 6ba17e4ed..0fa43c17f 100644 --- a/general/betheprofessional/cog.py +++ b/general/betheprofessional/cog.py @@ -1,9 +1,8 @@ -import logging import string from collections import Counter from typing import List, Union, Optional, Dict -from discord import Member, Embed, Role, Message, Guild +from discord import Member, Embed, Role, Message from discord.ext import commands, tasks from discord.ext.commands import guild_only, Context, CommandError, UserInputError @@ -11,6 +10,7 @@ from PyDrocsid.command import reply from PyDrocsid.database import db, select, db_wrapper from PyDrocsid.embeds import send_long_embed +from PyDrocsid.logger import get_logger from PyDrocsid.translations import t from PyDrocsid.util import calculate_edit_distance from .colors import Colors @@ -18,7 +18,6 @@ from .permissions import BeTheProfessionalPermission from ...contributor import Contributor from ...pubsub import send_to_changelog -from PyDrocsid.logger import get_logger tg = t.g t = t.betheprofessional From f315a1ad46844c907650f92d0c896a11e7d47396 Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Sat, 26 Jun 2021 07:27:23 +0200 Subject: [PATCH 29/68] Added Topic Role Update Command --- general/betheprofessional/cog.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/general/betheprofessional/cog.py b/general/betheprofessional/cog.py index 0fa43c17f..6ed3b0976 100644 --- a/general/betheprofessional/cog.py +++ b/general/betheprofessional/cog.py @@ -291,9 +291,15 @@ async def topic(self, ctx: Context, topic_name: str, message: Optional[Message]) else: await message.reply(mention) - @tasks.loop(seconds=30) # SET hours to 24 in Prod + @commands.command(aliases=["topic_update", "update_roles"]) + @guild_only() + @BeTheProfessionalPermission.manage.check + async def topic_update_roles(self, ctx: Context): + await self.update_roles() + await reply(ctx, 'Updated Topic Roles') + + @tasks.loop(hours=24) @db_wrapper - # TODO Change to Config async def update_roles(self): logger.info("Started Update Role Loop") topic_count: List[int] = [] From 387348049dd0ca9f447474ebf3545e77a6ad5a93 Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Mon, 9 Aug 2021 16:20:29 +0200 Subject: [PATCH 30/68] Fixed BeTheProfessional Update Role and Refactored --- general/betheprofessional/cog.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/general/betheprofessional/cog.py b/general/betheprofessional/cog.py index 6ed3b0976..71bb16772 100644 --- a/general/betheprofessional/cog.py +++ b/general/betheprofessional/cog.py @@ -296,20 +296,20 @@ async def topic(self, ctx: Context, topic_name: str, message: Optional[Message]) @BeTheProfessionalPermission.manage.check async def topic_update_roles(self, ctx: Context): await self.update_roles() - await reply(ctx, 'Updated Topic Roles') + await reply(ctx, "Updated Topic Roles") @tasks.loop(hours=24) @db_wrapper async def update_roles(self): logger.info("Started Update Role Loop") - topic_count: List[int] = [] + topic_count: Dict[int, int] = {} for topic in await db.all(select(BTPTopic)): - for _ in range(await db.count(select(BTPUser).filter_by(topic=topic.id))): - topic_count.append(topic.id) - topic_count: Counter = Counter(topic_count) + topic_count[topic.id] = await db.count(select(BTPUser).filter_by(topic=topic.id)) top_topics: List[int] = [] - for topic_count in sorted(topic_count)[: (100 if len(topic_count) >= 100 else len(topic_count))]: - top_topics.append(topic_count) + for topic_id in sorted(topic_count, key=lambda x: topic_count[x], reverse=True)[ + : (100 if len(topic_count) >= 100 else len(topic_count)) + ]: + top_topics.append(topic_id) for topic in await db.all(select(BTPTopic).filter(BTPTopic.role_id != None)): # noqa: E711 if topic.id not in top_topics: await self.bot.guilds[0].get_role(topic.role_id).delete() From c852e94a1324b5b63f1f20fc86ed7e01048dc40a Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Tue, 5 Oct 2021 19:20:24 +0200 Subject: [PATCH 31/68] Made Topic Names complete Unique, Fixed Role Assign, Fixed Role Delete --- general/betheprofessional/cog.py | 67 ++++++++++++++++++++++---------- 1 file changed, 47 insertions(+), 20 deletions(-) diff --git a/general/betheprofessional/cog.py b/general/betheprofessional/cog.py index 05b5a48fc..0cfb5b0db 100644 --- a/general/betheprofessional/cog.py +++ b/general/betheprofessional/cog.py @@ -1,5 +1,4 @@ import string -from collections import Counter from typing import List, Union, Optional, Dict from discord import Member, Embed, Role, Message @@ -54,22 +53,26 @@ async def split_parents(topics: List[str], assignable: bool) -> List[tuple[str, async def parse_topics(topics_str: str) -> List[BTPTopic]: topics: List[BTPTopic] = [] all_topics: List[BTPTopic] = await get_topics() - for topic in split_topics(topics_str): - query = select(BTPTopic).filter_by(name=topic) - topic_db = await db.first(query) - if not (await db.exists(query)) and len(all_topics) > 0: + if len(all_topics) == 0: + raise CommandError(t.no_topics_registered) + + for topic_name in split_topics(topics_str): + topic = await db.first(select(BTPTopic).filter_by(name=topic_name)) + + if topic is None and len(all_topics) > 0: def dist(name: str) -> int: - return calculate_edit_distance(name.lower(), topic.lower()) + return calculate_edit_distance(name.lower(), topic_name.lower()) best_dist, best_match = min((dist(r.name), r.name) for r in all_topics) if best_dist <= 5: - raise CommandError(t.topic_not_found_did_you_mean(topic, best_match)) + raise CommandError(t.topic_not_found_did_you_mean(topic_name, best_match)) - raise CommandError(t.topic_not_found(topic)) - elif not (await db.exists(query)): + raise CommandError(t.topic_not_found(topic_name)) + elif topic is None: raise CommandError(t.no_topics_registered) - topics.append(topic_db) + topics.append(topic) + return topics @@ -81,7 +84,13 @@ async def get_topics() -> List[BTPTopic]: class BeTheProfessionalCog(Cog, name="BeTheProfessional"): - CONTRIBUTORS = [Contributor.Defelo, Contributor.wolflu, Contributor.MaxiHuHe04, Contributor.AdriBloober] + CONTRIBUTORS = [ + Contributor.Defelo, + Contributor.wolflu, + Contributor.MaxiHuHe04, + Contributor.AdriBloober, + Contributor.Tert0 + ] async def on_ready(self): self.update_roles.start() @@ -152,8 +161,15 @@ async def assign_topics(self, ctx: Context, *, topics: str): if (await db.exists(select(BTPTopic).filter_by(id=topic.id))) and not (await db.exists(select(BTPUser).filter_by(user_id=member.id, topic=topic.id))) # noqa: W503 ] + + roles: List[Role] = [] + for topic in topics: await BTPUser.create(member.id, topic.id) + if topic.role_id: + roles.append(ctx.guild.get_role(topic.role_id)) + await ctx.author.add_roles(*roles) + embed = Embed(title=t.betheprofessional, colour=Colors.BeTheProfessional) embed.description = t.topics_added(cnt=len(topics)) if not topics: @@ -177,8 +193,14 @@ async def unassign_topics(self, ctx: Context, *, topics: str): if await db.exists(select(BTPUser).filter_by(user_id=member.id, topic=topic.id)): affected_topics.append(topic) + roles: List[Role] = [] + for topic in affected_topics: await db.delete(await db.first(select(BTPUser).filter_by(topic=topic.id))) + if topic.role_id: + roles.append(ctx.guild.get_role(topic.role_id)) + + await ctx.author.remove_roles(*roles) embed = Embed(title=t.betheprofessional, colour=Colors.BeTheProfessional) embed.description = t.topics_removed(cnt=len(affected_topics)) @@ -205,12 +227,8 @@ async def register_topics(self, ctx: Context, *, topics: str, assignable: bool = if any(c not in valid_chars for c in topic[0]): raise CommandError(t.topic_invalid_chars(topic)) - if await db.exists( - select(BTPTopic).filter_by(name=topic[0], parent=topic[2][-1].id if len(topic[2]) > 0 else None), - ): - raise CommandError( - t.topic_already_registered(f"{topic[1]}/{topic[2][-1].name + '/' if topic[1] else ''}{topic[0]}"), - ) + if await db.exists(select(BTPTopic).filter_by(name=topic[0])): + raise CommandError(t.topic_already_registered(topic[0])) else: registered_topics.append(topic) @@ -268,7 +286,7 @@ async def delete_topics(self, ctx: Context, *, topics: str): embed.description = t.topics_unregistered(cnt=len(delete_topics)) await send_to_changelog( ctx.guild, - t.log_topics_unregistered(cnt=len(delete_topics), topics=", ".join(f"`{r}`" for r in delete_topics)), + t.log_topics_unregistered(cnt=len(delete_topics), topics=", ".join(f"`{t.name}`" for t in delete_topics)), ) await send_long_embed(ctx, embed) @@ -312,10 +330,19 @@ async def update_roles(self): : (100 if len(topic_count) >= 100 else len(topic_count)) ]: top_topics.append(topic_id) - for topic in await db.all(select(BTPTopic).filter(BTPTopic.role_id != None)): # noqa: E711 + for topic in await db.all(select(BTPTopic).filter(BTPTopic.role_id is not None)): # type: BTPTopic if topic.id not in top_topics: - await self.bot.guilds[0].get_role(topic.role_id).delete() + if topic.role_id is not None: + await self.bot.guilds[0].get_role(topic.role_id).delete() + topic.role_id = None for top_topic in top_topics: if (topic := await db.first(select(BTPTopic).filter_by(id=top_topic, role_id=None))) is not None: topic.role_id = (await self.bot.guilds[0].create_role(name=topic.name)).id + for member_id in await db.all(select(BTPUser).filter_by(topic=topic.id)): + member = await self.bot.guilds[0].get_member(member_id) + if member: + role = self.bot.guilds[0].get_role(topic.role_id) + await member.add_roles(role) + else: + raise Exception # TODO Error Handling logger.info("Created Top Topic Roles") From ce73f3317f537a55f7aed37697011f8414a7d50c Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Mon, 11 Oct 2021 20:50:30 +0200 Subject: [PATCH 32/68] Fix PEP8 --- general/betheprofessional/cog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/general/betheprofessional/cog.py b/general/betheprofessional/cog.py index 0cfb5b0db..bd49bda68 100644 --- a/general/betheprofessional/cog.py +++ b/general/betheprofessional/cog.py @@ -89,7 +89,7 @@ class BeTheProfessionalCog(Cog, name="BeTheProfessional"): Contributor.wolflu, Contributor.MaxiHuHe04, Contributor.AdriBloober, - Contributor.Tert0 + Contributor.Tert0, ] async def on_ready(self): From 433fc673fb8c00507eb1a0274aa14a959eb42076 Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Sat, 8 Jan 2022 15:05:21 +0100 Subject: [PATCH 33/68] Fixed DB Models, Updated Command Descriptions, Improved Top Topic Role Loop and added role added on rejoin --- general/betheprofessional/cog.py | 70 +++++++++++++++++++-------- general/betheprofessional/models.py | 15 +++--- general/betheprofessional/settings.py | 6 +++ 3 files changed, 64 insertions(+), 27 deletions(-) create mode 100644 general/betheprofessional/settings.py diff --git a/general/betheprofessional/cog.py b/general/betheprofessional/cog.py index 171042dec..c1040bf68 100644 --- a/general/betheprofessional/cog.py +++ b/general/betheprofessional/cog.py @@ -15,6 +15,7 @@ from .colors import Colors from .models import BTPUser, BTPTopic from .permissions import BeTheProfessionalPermission +from .settings import BeTheProfessionalSettings from ...contributor import Contributor from ...pubsub import send_to_changelog @@ -49,7 +50,6 @@ async def split_parents(topics: List[str], assignable: bool) -> List[tuple[str, result.append((topic, assignable, parents)) return result - async def parse_topics(topics_str: str) -> List[BTPTopic]: topics: List[BTPTopic] = [] all_topics: List[BTPTopic] = await get_topics() @@ -99,13 +99,13 @@ async def on_ready(self): @guild_only() async def list_topics(self, ctx: Context, parent_topic: Optional[str]): """ - list all registered topics + list all direct children topics of the parent """ parent: Union[BTPTopic, None, CommandError] = ( None if parent_topic is None else await db.first(select(BTPTopic).filter_by(name=parent_topic)) - or CommandError(t.topic_not_found(parent_topic)) # noqa: W503 + or CommandError(t.topic_not_found(parent_topic)) # noqa: W503 ) if isinstance(parent, CommandError): raise parent @@ -131,7 +131,7 @@ async def list_topics(self, ctx: Context, parent_topic: Optional[str]): for root_topic in sorted_topics.keys(): embed.add_field( - name=root_topic.title(), + name=root_topic, value=", ".join( [ f"`{topic.name}" @@ -159,7 +159,7 @@ async def assign_topics(self, ctx: Context, *, topics: str): topic for topic in await parse_topics(topics) if (await db.exists(select(BTPTopic).filter_by(id=topic.id))) - and not (await db.exists(select(BTPUser).filter_by(user_id=member.id, topic=topic.id))) # noqa: W503 + and not (await db.exists(select(BTPUser).filter_by(user_id=member.id, topic=topic.id))) # noqa: W503 ] roles: List[Role] = [] @@ -209,19 +209,19 @@ async def unassign_topics(self, ctx: Context, *, topics: str): @commands.command(name="*") @BeTheProfessionalPermission.manage.check @guild_only() - async def register_topics(self, ctx: Context, *, topics: str, assignable: bool = True): + async def register_topics(self, ctx: Context, *, topic_paths: str, assignable: bool = True): """ - register one or more new topics + register one or more new topics by path """ - names = split_topics(topics) - topics: List[tuple[str, bool, Optional[list[BTPTopic]]]] = await split_parents(names, assignable) - if not names or not topics: + names = split_topics(topic_paths) + topic_paths: List[tuple[str, bool, Optional[list[BTPTopic]]]] = await split_parents(names, assignable) + if not names or not topic_paths: raise UserInputError valid_chars = set(string.ascii_letters + string.digits + " !#$%&'()+-./:<=>?[\\]^_`{|}~") registered_topics: List[tuple[str, bool, Optional[list[BTPTopic]]]] = [] - for topic in topics: + for topic in topic_paths: if len(topic) > 100: raise CommandError(t.topic_too_long(topic)) if any(c not in valid_chars for c in topic[0]): @@ -270,7 +270,7 @@ async def delete_topics(self, ctx: Context, *, topics: str): btp_topic = await db.first(select(BTPTopic).filter_by(name=topic)) delete_topics.append(btp_topic) for child_topic in await db.all( - select(BTPTopic).filter_by(parent=btp_topic.id), + select(BTPTopic).filter_by(parent=btp_topic.id), ): # TODO Recursive? Fix more level childs delete_topics.insert(0, child_topic) for topic in delete_topics: @@ -293,6 +293,10 @@ async def delete_topics(self, ctx: Context, *, topics: str): @commands.command() @guild_only() async def topic(self, ctx: Context, topic_name: str, message: Optional[Message]): + """ + pings the specified topic + """ + topic: BTPTopic = await db.first(select(BTPTopic).filter_by(name=topic_name)) mention: str if topic is None: @@ -315,34 +319,58 @@ async def topic(self, ctx: Context, topic_name: str, message: Optional[Message]) @guild_only() @BeTheProfessionalPermission.manage.check async def topic_update_roles(self, ctx: Context): + """ + updates the topic roles manually + """ + await self.update_roles() await reply(ctx, "Updated Topic Roles") @tasks.loop(hours=24) @db_wrapper async def update_roles(self): + RoleCreateMinUsers = await BeTheProfessionalSettings.RoleCreateMinUsers.get() + logger.info("Started Update Role Loop") topic_count: Dict[int, int] = {} + for topic in await db.all(select(BTPTopic)): topic_count[topic.id] = await db.count(select(BTPUser).filter_by(topic=topic.id)) - top_topics: List[int] = [] - for topic_id in sorted(topic_count, key=lambda x: topic_count[x], reverse=True)[ - : (100 if len(topic_count) >= 100 else len(topic_count)) - ]: - top_topics.append(topic_id) + + # Sort Topics By Count, Keep only Topics with a Count of BeTheProfessionalSettings.RoleCreateMinUsers or above + # Limit Roles to BeTheProfessionalSettings.RoleLimit + top_topics: List[int] = list( + filter( + lambda topic_id: topic_count[topic_id] >= RoleCreateMinUsers, + sorted(topic_count, key=lambda x: topic_count[x], reverse=True), + ) + )[:await BeTheProfessionalSettings.RoleLimit.get()] + + # Delete old Top Topic Roles for topic in await db.all(select(BTPTopic).filter(BTPTopic.role_id is not None)): # type: BTPTopic if topic.id not in top_topics: if topic.role_id is not None: await self.bot.guilds[0].get_role(topic.role_id).delete() topic.role_id = None + + # Create new Topic Role and add Role to Users + # TODO Optimize from `LOOP all topics: LOOP all Members: add role` + # to `LOOP all Members with Topic: add all roles` and separate the role creating for top_topic in top_topics: if (topic := await db.first(select(BTPTopic).filter_by(id=top_topic, role_id=None))) is not None: topic.role_id = (await self.bot.guilds[0].create_role(name=topic.name)).id - for member_id in await db.all(select(BTPUser).filter_by(topic=topic.id)): - member = await self.bot.guilds[0].get_member(member_id) + for btp_user in await db.all(select(BTPUser).filter_by(topic=topic.id)): + member = await self.bot.guilds[0].fetch_member(btp_user.user_id) if member: role = self.bot.guilds[0].get_role(topic.role_id) - await member.add_roles(role) + await member.add_roles(role, atomic=False) else: - raise Exception # TODO Error Handling + pass + logger.info("Created Top Topic Roles") + + async def on_member_join(self, member: Member): + topics: List[BTPUser] = await db.all(select(BTPUser).filter_by(user_id=member.id)) + role_ids: List[int] = [(await db.first(select(BTPTopic).filter_by(id=topic))).role_id for topic in topics] + roles: List[Role] = [self.bot.guilds[0].get_role(role_id) for role_id in role_ids] + await member.add_roles(*roles, atomic=False) diff --git a/general/betheprofessional/models.py b/general/betheprofessional/models.py index 55b0f6928..b3bce618f 100644 --- a/general/betheprofessional/models.py +++ b/general/betheprofessional/models.py @@ -1,6 +1,9 @@ from typing import Union, Optional +from PyDrocsid.database import db, Base +from sqlalchemy import Column, BigInteger, Boolean, Integer, String, ForeignKey -class BTPTopic(db.Base): + +class BTPTopic(Base): __tablename__ = "btp_topic" id: Union[Column, int] = Column(Integer, primary_key=True) @@ -11,17 +14,17 @@ class BTPTopic(db.Base): @staticmethod async def create( - name: str, - role_id: Union[int, None], - assignable: bool, - parent: Optional[Union[int, None]], + name: str, + role_id: Union[int, None], + assignable: bool, + parent: Optional[Union[int, None]], ) -> "BTPTopic": row = BTPTopic(name=name, role_id=role_id, parent=parent, assignable=assignable) await db.add(row) return row -class BTPUser(db.Base): +class BTPUser(Base): __tablename__ = "btp_users" id: Union[Column, int] = Column(Integer, primary_key=True) diff --git a/general/betheprofessional/settings.py b/general/betheprofessional/settings.py new file mode 100644 index 000000000..084731c04 --- /dev/null +++ b/general/betheprofessional/settings.py @@ -0,0 +1,6 @@ +from PyDrocsid.settings import Settings + + +class BeTheProfessionalSettings(Settings): + RoleLimit = 100 + RoleCreateMinUsers = 1 # TODO From 2b4f90dee2d36a27c8999ad2f57c0af050bae139 Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Sun, 9 Jan 2022 13:57:07 +0100 Subject: [PATCH 34/68] Added BTP Leaderboard Command --- general/betheprofessional/cog.py | 84 ++++++++++++++++++- general/betheprofessional/settings.py | 3 + general/betheprofessional/translations/en.yml | 10 +++ 3 files changed, 94 insertions(+), 3 deletions(-) diff --git a/general/betheprofessional/cog.py b/general/betheprofessional/cog.py index c1040bf68..fa09b64a0 100644 --- a/general/betheprofessional/cog.py +++ b/general/betheprofessional/cog.py @@ -1,10 +1,12 @@ +import io import string from typing import List, Union, Optional, Dict -from discord import Member, Embed, Role, Message +from discord import Member, Embed, Role, Message, File from discord.ext import commands, tasks from discord.ext.commands import guild_only, Context, CommandError, UserInputError +import PyDrocsid.embeds from PyDrocsid.cog import Cog from PyDrocsid.command import reply from PyDrocsid.database import db, select, db_wrapper @@ -17,7 +19,7 @@ from .permissions import BeTheProfessionalPermission from .settings import BeTheProfessionalSettings from ...contributor import Contributor -from ...pubsub import send_to_changelog +from ...pubsub import send_to_changelog, send_alert tg = t.g t = t.betheprofessional @@ -50,6 +52,7 @@ async def split_parents(topics: List[str], assignable: bool) -> List[tuple[str, result.append((topic, assignable, parents)) return result + async def parse_topics(topics_str: str) -> List[BTPTopic]: topics: List[BTPTopic] = [] all_topics: List[BTPTopic] = await get_topics() @@ -315,6 +318,77 @@ async def topic(self, ctx: Context, topic_name: str, message: Optional[Message]) else: await message.reply(mention) + @commands.group() + @guild_only() + async def btp(self, ctx: Context): + if ctx.invoked_subcommand is None: + raise UserInputError + + @btp.command(aliases=["lb"]) + @guild_only() + async def leaderboard(self, ctx: Context, n: Optional[int] = None): + """ + lists the top n topics + """ + + default_n = await BeTheProfessionalSettings.LeaderboardDefaultN.get() + max_n = await BeTheProfessionalSettings.LeaderboardMaxN.get() + if n is None: + n = default_n + if default_n > max_n: + await send_alert(ctx.guild, t.leaderboard_default_n_bigger_than_max_n) + raise CommandError(t.leaderboard_configuration_error) + if n > max_n: + raise CommandError(t.leaderboard_n_too_big(n, max_n)) + if n <= 0: + raise CommandError(t.leaderboard_n_zero_error) + + topic_count: Dict[int, int] = {} + + for topic in await db.all(select(BTPTopic)): + topic_count[topic.id] = await db.count(select(BTPUser).filter_by(topic=topic.id)) + + top_topics: List[int] = list( + sorted(topic_count, key=lambda x: topic_count[x], reverse=True), + )[:n] + + if len(top_topics) == 0: + raise CommandError(t.no_topics_registered) + + name_field = t.leaderboard_colmn_name + users_field = t.leaderboard_colmn_users + + rank_len = len(str(len(top_topics))) + 1 + name_len = max(max([len(topic.name) for topic in await db.all(select(BTPTopic))]), len(name_field)) + + TABLE_SPACING = 2 + + leaderboard_rows: list[str] = [] + for i, topic_id in enumerate(top_topics): + topic: BTPTopic = await db.first(select(BTPTopic).filter_by(id=topic_id)) + users: int = topic_count[topic_id] + name: str = topic.name.ljust(name_len, ' ') + rank: str = "#" + str(i+1).rjust(rank_len - 1, '0') + leaderboard_rows.append(f"{rank}{' ' * TABLE_SPACING}{name}{' ' * TABLE_SPACING}{users}") + + rank_spacing = ' ' * (rank_len + TABLE_SPACING) + name_spacing = ' ' * (name_len + TABLE_SPACING - len(name_field)) + + header: str = f"{rank_spacing}{name_field}{name_spacing}{users_field}\n" + leaderboard: str = header + '\n'.join(leaderboard_rows) + + embed = Embed(title=t.leaderboard_title(n), description=f"```css\n{leaderboard}\n```") + + if len(embed.description) > PyDrocsid.embeds.EmbedLimits.DESCRIPTION or True: + embed.description = None + with io.StringIO() as leaderboard_file: + leaderboard_file.write(leaderboard) + leaderboard_file.seek(0) + file = File(fp=leaderboard_file, filename="output.css") + await reply(ctx, embed=embed, file=file) + else: + await reply(ctx, embed=embed) + @commands.command(aliases=["topic_update", "update_roles"]) @guild_only() @BeTheProfessionalPermission.manage.check @@ -363,7 +437,11 @@ async def update_roles(self): member = await self.bot.guilds[0].fetch_member(btp_user.user_id) if member: role = self.bot.guilds[0].get_role(topic.role_id) - await member.add_roles(role, atomic=False) + if role: + await member.add_roles(role, atomic=False) + else: + await send_alert(self.bot.guilds[0], + t.fetching_topic_role_failed(topic.name, topic.role_id)) else: pass diff --git a/general/betheprofessional/settings.py b/general/betheprofessional/settings.py index 084731c04..8903bd26b 100644 --- a/general/betheprofessional/settings.py +++ b/general/betheprofessional/settings.py @@ -4,3 +4,6 @@ class BeTheProfessionalSettings(Settings): RoleLimit = 100 RoleCreateMinUsers = 1 # TODO + + LeaderboardDefaultN = 10 + LeaderboardMaxN = 20 diff --git a/general/betheprofessional/translations/en.yml b/general/betheprofessional/translations/en.yml index 831ac0e9b..0b50d69fd 100644 --- a/general/betheprofessional/translations/en.yml +++ b/general/betheprofessional/translations/en.yml @@ -45,3 +45,13 @@ parent_format_help: "Please write `[Parents/]Topic-Name`" group_not_parent_group: "The group `{}` is not the same as the group of the Parent `{}`" nobody_has_topic: "Nobody has the Topic `{}`" +leaderboard_n_too_big: "The given `N={}` is bigger than the maximum `N={}`" +leaderboard_default_n_bigger_than_max_n: "The default N is bigger than the maximum N" +leaderboard_configuration_error: "Internal Configuration Error" +leaderboard_n_zero_error: "N cant be zero or less!" + +fetching_topic_role_failed: "Failed to fetch Role of the Topic `{}` with the Role ID `{}`" + +leaderboard_colmn_name: "NAME" +leaderboard_colmn_users: "USERS" +leaderboard_title: "Top `{}` - Most assigned Topics" \ No newline at end of file From 23732c70fe0048d360f0561b22bed245b95e890d Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Sun, 9 Jan 2022 14:09:51 +0100 Subject: [PATCH 35/68] Added Redis Leaderboard Cache, Added Bypass Cache Permission, Added Bypass N Limit Permission --- general/betheprofessional/cog.py | 65 +++++++++++-------- general/betheprofessional/permissions.py | 2 + general/betheprofessional/translations/en.yml | 4 +- 3 files changed, 44 insertions(+), 27 deletions(-) diff --git a/general/betheprofessional/cog.py b/general/betheprofessional/cog.py index fa09b64a0..b7a8d88a8 100644 --- a/general/betheprofessional/cog.py +++ b/general/betheprofessional/cog.py @@ -11,7 +11,9 @@ from PyDrocsid.command import reply from PyDrocsid.database import db, select, db_wrapper from PyDrocsid.embeds import send_long_embed +from PyDrocsid.environment import CACHE_TTL from PyDrocsid.logger import get_logger +from PyDrocsid.redis import redis from PyDrocsid.translations import t from PyDrocsid.util import calculate_edit_distance from .colors import Colors @@ -326,7 +328,7 @@ async def btp(self, ctx: Context): @btp.command(aliases=["lb"]) @guild_only() - async def leaderboard(self, ctx: Context, n: Optional[int] = None): + async def leaderboard(self, ctx: Context, n: Optional[int] = None, use_cache: bool = True): """ lists the top n topics """ @@ -338,44 +340,55 @@ async def leaderboard(self, ctx: Context, n: Optional[int] = None): if default_n > max_n: await send_alert(ctx.guild, t.leaderboard_default_n_bigger_than_max_n) raise CommandError(t.leaderboard_configuration_error) - if n > max_n: + if n > max_n and not BeTheProfessionalPermission.bypass_leaderboard_n_limit.check_permissions(ctx.author): raise CommandError(t.leaderboard_n_too_big(n, max_n)) if n <= 0: raise CommandError(t.leaderboard_n_zero_error) - topic_count: Dict[int, int] = {} + cached_leaderboard: Optional[str] = None - for topic in await db.all(select(BTPTopic)): - topic_count[topic.id] = await db.count(select(BTPUser).filter_by(topic=topic.id)) + if use_cache: + if not BeTheProfessionalPermission.bypass_leaderboard_cache.check_permissions(ctx.author): + raise CommandError(t.missing_cache_bypass_permission) + cached_leaderboard = await redis.get(f"btp:leaderboard:n:{n}") - top_topics: List[int] = list( - sorted(topic_count, key=lambda x: topic_count[x], reverse=True), - )[:n] + if cached_leaderboard is None: + topic_count: Dict[int, int] = {} - if len(top_topics) == 0: - raise CommandError(t.no_topics_registered) + for topic in await db.all(select(BTPTopic)): + topic_count[topic.id] = await db.count(select(BTPUser).filter_by(topic=topic.id)) - name_field = t.leaderboard_colmn_name - users_field = t.leaderboard_colmn_users + top_topics: List[int] = list( + sorted(topic_count, key=lambda x: topic_count[x], reverse=True), + )[:n] + + if len(top_topics) == 0: + raise CommandError(t.no_topics_registered) - rank_len = len(str(len(top_topics))) + 1 - name_len = max(max([len(topic.name) for topic in await db.all(select(BTPTopic))]), len(name_field)) + name_field = t.leaderboard_colmn_name + users_field = t.leaderboard_colmn_users - TABLE_SPACING = 2 + rank_len = len(str(len(top_topics))) + 1 + name_len = max(max([len(topic.name) for topic in await db.all(select(BTPTopic))]), len(name_field)) - leaderboard_rows: list[str] = [] - for i, topic_id in enumerate(top_topics): - topic: BTPTopic = await db.first(select(BTPTopic).filter_by(id=topic_id)) - users: int = topic_count[topic_id] - name: str = topic.name.ljust(name_len, ' ') - rank: str = "#" + str(i+1).rjust(rank_len - 1, '0') - leaderboard_rows.append(f"{rank}{' ' * TABLE_SPACING}{name}{' ' * TABLE_SPACING}{users}") + TABLE_SPACING = 2 - rank_spacing = ' ' * (rank_len + TABLE_SPACING) - name_spacing = ' ' * (name_len + TABLE_SPACING - len(name_field)) + leaderboard_rows: list[str] = [] + for i, topic_id in enumerate(top_topics): + topic: BTPTopic = await db.first(select(BTPTopic).filter_by(id=topic_id)) + users: int = topic_count[topic_id] + name: str = topic.name.ljust(name_len, ' ') + rank: str = "#" + str(i+1).rjust(rank_len - 1, '0') + leaderboard_rows.append(f"{rank}{' ' * TABLE_SPACING}{name}{' ' * TABLE_SPACING}{users}") - header: str = f"{rank_spacing}{name_field}{name_spacing}{users_field}\n" - leaderboard: str = header + '\n'.join(leaderboard_rows) + rank_spacing = ' ' * (rank_len + TABLE_SPACING) + name_spacing = ' ' * (name_len + TABLE_SPACING - len(name_field)) + + header: str = f"{rank_spacing}{name_field}{name_spacing}{users_field}\n" + leaderboard: str = header + '\n'.join(leaderboard_rows) + await redis.setex(f"btp:leaderboard:n:{n}", CACHE_TTL, leaderboard) + else: + leaderboard: str = cached_leaderboard embed = Embed(title=t.leaderboard_title(n), description=f"```css\n{leaderboard}\n```") diff --git a/general/betheprofessional/permissions.py b/general/betheprofessional/permissions.py index 2889a1a47..e6acf68bb 100644 --- a/general/betheprofessional/permissions.py +++ b/general/betheprofessional/permissions.py @@ -10,3 +10,5 @@ def description(self) -> str: return t.betheprofessional.permissions[self.name] manage = auto() + bypass_leaderboard_cache = auto() + bypass_leaderboard_n_limit = auto() diff --git a/general/betheprofessional/translations/en.yml b/general/betheprofessional/translations/en.yml index 0b50d69fd..92db00614 100644 --- a/general/betheprofessional/translations/en.yml +++ b/general/betheprofessional/translations/en.yml @@ -1,6 +1,8 @@ permissions: manage: manage betheprofessional roles - + bypass_leaderboard_cache: bypass leadboard cache + bypass_leaderboard_n_limit: bypass leaderboard n limit +missing_cache_bypass_permission: "Missing Cache bypass Permission" # betheprofessional betheprofessional: BeTheProfessional youre_not_the_first_one: "Topic `{}` not found.\nYou're not the first one to try this, {}" From 4ca2a3c67072cc92d209374c13c40415b2a515a5 Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Sun, 9 Jan 2022 14:15:21 +0100 Subject: [PATCH 36/68] Refactored Code --- general/betheprofessional/cog.py | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/general/betheprofessional/cog.py b/general/betheprofessional/cog.py index b7a8d88a8..b3daafb29 100644 --- a/general/betheprofessional/cog.py +++ b/general/betheprofessional/cog.py @@ -1,6 +1,6 @@ import io import string -from typing import List, Union, Optional, Dict +from typing import List, Union, Optional, Dict, Final from discord import Member, Embed, Role, Message, File from discord.ext import commands, tasks @@ -358,9 +358,7 @@ async def leaderboard(self, ctx: Context, n: Optional[int] = None, use_cache: bo for topic in await db.all(select(BTPTopic)): topic_count[topic.id] = await db.count(select(BTPUser).filter_by(topic=topic.id)) - top_topics: List[int] = list( - sorted(topic_count, key=lambda x: topic_count[x], reverse=True), - )[:n] + top_topics: List[int] = sorted(topic_count, key=lambda x: topic_count[x], reverse=True)[:n] if len(top_topics) == 0: raise CommandError(t.no_topics_registered) @@ -371,14 +369,14 @@ async def leaderboard(self, ctx: Context, n: Optional[int] = None, use_cache: bo rank_len = len(str(len(top_topics))) + 1 name_len = max(max([len(topic.name) for topic in await db.all(select(BTPTopic))]), len(name_field)) - TABLE_SPACING = 2 + TABLE_SPACING: Final = 2 leaderboard_rows: list[str] = [] for i, topic_id in enumerate(top_topics): topic: BTPTopic = await db.first(select(BTPTopic).filter_by(id=topic_id)) users: int = topic_count[topic_id] name: str = topic.name.ljust(name_len, ' ') - rank: str = "#" + str(i+1).rjust(rank_len - 1, '0') + rank: str = "#" + str(i + 1).rjust(rank_len - 1, '0') leaderboard_rows.append(f"{rank}{' ' * TABLE_SPACING}{name}{' ' * TABLE_SPACING}{users}") rank_spacing = ' ' * (rank_len + TABLE_SPACING) @@ -448,15 +446,14 @@ async def update_roles(self): topic.role_id = (await self.bot.guilds[0].create_role(name=topic.name)).id for btp_user in await db.all(select(BTPUser).filter_by(topic=topic.id)): member = await self.bot.guilds[0].fetch_member(btp_user.user_id) - if member: - role = self.bot.guilds[0].get_role(topic.role_id) - if role: - await member.add_roles(role, atomic=False) - else: - await send_alert(self.bot.guilds[0], - t.fetching_topic_role_failed(topic.name, topic.role_id)) + if not member: + continue + role = self.bot.guilds[0].get_role(topic.role_id) + if role: + await member.add_roles(role, atomic=False) else: - pass + await send_alert(self.bot.guilds[0], + t.fetching_topic_role_failed(topic.name, topic.role_id)) logger.info("Created Top Topic Roles") From 7c5f7bd40d37bbdb40e8854375575201b195fd84 Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Sun, 9 Jan 2022 14:19:22 +0100 Subject: [PATCH 37/68] Reformatted with black --- general/betheprofessional/cog.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/general/betheprofessional/cog.py b/general/betheprofessional/cog.py index b3daafb29..122833674 100644 --- a/general/betheprofessional/cog.py +++ b/general/betheprofessional/cog.py @@ -66,6 +66,7 @@ async def parse_topics(topics_str: str) -> List[BTPTopic]: topic = await db.first(select(BTPTopic).filter_by(name=topic_name)) if topic is None and len(all_topics) > 0: + def dist(name: str) -> int: return calculate_edit_distance(name.lower(), topic_name.lower()) @@ -110,7 +111,7 @@ async def list_topics(self, ctx: Context, parent_topic: Optional[str]): None if parent_topic is None else await db.first(select(BTPTopic).filter_by(name=parent_topic)) - or CommandError(t.topic_not_found(parent_topic)) # noqa: W503 + or CommandError(t.topic_not_found(parent_topic)) # noqa: W503 ) if isinstance(parent, CommandError): raise parent @@ -164,7 +165,7 @@ async def assign_topics(self, ctx: Context, *, topics: str): topic for topic in await parse_topics(topics) if (await db.exists(select(BTPTopic).filter_by(id=topic.id))) - and not (await db.exists(select(BTPUser).filter_by(user_id=member.id, topic=topic.id))) # noqa: W503 + and not (await db.exists(select(BTPUser).filter_by(user_id=member.id, topic=topic.id))) # noqa: W503 ] roles: List[Role] = [] @@ -275,7 +276,7 @@ async def delete_topics(self, ctx: Context, *, topics: str): btp_topic = await db.first(select(BTPTopic).filter_by(name=topic)) delete_topics.append(btp_topic) for child_topic in await db.all( - select(BTPTopic).filter_by(parent=btp_topic.id), + select(BTPTopic).filter_by(parent=btp_topic.id), ): # TODO Recursive? Fix more level childs delete_topics.insert(0, child_topic) for topic in delete_topics: @@ -375,15 +376,15 @@ async def leaderboard(self, ctx: Context, n: Optional[int] = None, use_cache: bo for i, topic_id in enumerate(top_topics): topic: BTPTopic = await db.first(select(BTPTopic).filter_by(id=topic_id)) users: int = topic_count[topic_id] - name: str = topic.name.ljust(name_len, ' ') - rank: str = "#" + str(i + 1).rjust(rank_len - 1, '0') + name: str = topic.name.ljust(name_len, " ") + rank: str = "#" + str(i + 1).rjust(rank_len - 1, "0") leaderboard_rows.append(f"{rank}{' ' * TABLE_SPACING}{name}{' ' * TABLE_SPACING}{users}") - rank_spacing = ' ' * (rank_len + TABLE_SPACING) - name_spacing = ' ' * (name_len + TABLE_SPACING - len(name_field)) + rank_spacing = " " * (rank_len + TABLE_SPACING) + name_spacing = " " * (name_len + TABLE_SPACING - len(name_field)) header: str = f"{rank_spacing}{name_field}{name_spacing}{users_field}\n" - leaderboard: str = header + '\n'.join(leaderboard_rows) + leaderboard: str = header + "\n".join(leaderboard_rows) await redis.setex(f"btp:leaderboard:n:{n}", CACHE_TTL, leaderboard) else: leaderboard: str = cached_leaderboard @@ -429,7 +430,7 @@ async def update_roles(self): lambda topic_id: topic_count[topic_id] >= RoleCreateMinUsers, sorted(topic_count, key=lambda x: topic_count[x], reverse=True), ) - )[:await BeTheProfessionalSettings.RoleLimit.get()] + )[: await BeTheProfessionalSettings.RoleLimit.get()] # Delete old Top Topic Roles for topic in await db.all(select(BTPTopic).filter(BTPTopic.role_id is not None)): # type: BTPTopic @@ -452,8 +453,7 @@ async def update_roles(self): if role: await member.add_roles(role, atomic=False) else: - await send_alert(self.bot.guilds[0], - t.fetching_topic_role_failed(topic.name, topic.role_id)) + await send_alert(self.bot.guilds[0], t.fetching_topic_role_failed(topic.name, topic.role_id)) logger.info("Created Top Topic Roles") From 6edfe584699a397af6fe5769dd853e25d4b1a0d6 Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Sun, 9 Jan 2022 14:22:58 +0100 Subject: [PATCH 38/68] PEP8 --- general/betheprofessional/cog.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/general/betheprofessional/cog.py b/general/betheprofessional/cog.py index 122833674..e06f2c34e 100644 --- a/general/betheprofessional/cog.py +++ b/general/betheprofessional/cog.py @@ -28,6 +28,8 @@ logger = get_logger(__name__) +LEADERBOARD_TABLE_SPACING: Final = 2 + def split_topics(topics: str) -> List[str]: return [topic for topic in map(str.strip, topics.replace(";", ",").split(",")) if topic] @@ -370,18 +372,16 @@ async def leaderboard(self, ctx: Context, n: Optional[int] = None, use_cache: bo rank_len = len(str(len(top_topics))) + 1 name_len = max(max([len(topic.name) for topic in await db.all(select(BTPTopic))]), len(name_field)) - TABLE_SPACING: Final = 2 - leaderboard_rows: list[str] = [] for i, topic_id in enumerate(top_topics): topic: BTPTopic = await db.first(select(BTPTopic).filter_by(id=topic_id)) users: int = topic_count[topic_id] name: str = topic.name.ljust(name_len, " ") rank: str = "#" + str(i + 1).rjust(rank_len - 1, "0") - leaderboard_rows.append(f"{rank}{' ' * TABLE_SPACING}{name}{' ' * TABLE_SPACING}{users}") + leaderboard_rows.append(f"{rank}{' ' * LEADERBOARD_TABLE_SPACING}{name}{' ' * LEADERBOARD_TABLE_SPACING}{users}") # noqa: E501 - rank_spacing = " " * (rank_len + TABLE_SPACING) - name_spacing = " " * (name_len + TABLE_SPACING - len(name_field)) + rank_spacing = " " * (rank_len + LEADERBOARD_TABLE_SPACING) + name_spacing = " " * (name_len + LEADERBOARD_TABLE_SPACING - len(name_field)) header: str = f"{rank_spacing}{name_field}{name_spacing}{users_field}\n" leaderboard: str = header + "\n".join(leaderboard_rows) @@ -415,7 +415,7 @@ async def topic_update_roles(self, ctx: Context): @tasks.loop(hours=24) @db_wrapper async def update_roles(self): - RoleCreateMinUsers = await BeTheProfessionalSettings.RoleCreateMinUsers.get() + role_create_min_users = await BeTheProfessionalSettings.RoleCreateMinUsers.get() logger.info("Started Update Role Loop") topic_count: Dict[int, int] = {} @@ -427,9 +427,9 @@ async def update_roles(self): # Limit Roles to BeTheProfessionalSettings.RoleLimit top_topics: List[int] = list( filter( - lambda topic_id: topic_count[topic_id] >= RoleCreateMinUsers, + lambda topic_id: topic_count[topic_id] >= role_create_min_users, sorted(topic_count, key=lambda x: topic_count[x], reverse=True), - ) + ), )[: await BeTheProfessionalSettings.RoleLimit.get()] # Delete old Top Topic Roles From f877b282b13df3d5d4e857b0121074d494e57b43 Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Sun, 9 Jan 2022 14:25:16 +0100 Subject: [PATCH 39/68] PEP8 --- general/betheprofessional/cog.py | 4 +++- general/betheprofessional/models.py | 8 ++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/general/betheprofessional/cog.py b/general/betheprofessional/cog.py index e06f2c34e..6b52123fd 100644 --- a/general/betheprofessional/cog.py +++ b/general/betheprofessional/cog.py @@ -378,7 +378,9 @@ async def leaderboard(self, ctx: Context, n: Optional[int] = None, use_cache: bo users: int = topic_count[topic_id] name: str = topic.name.ljust(name_len, " ") rank: str = "#" + str(i + 1).rjust(rank_len - 1, "0") - leaderboard_rows.append(f"{rank}{' ' * LEADERBOARD_TABLE_SPACING}{name}{' ' * LEADERBOARD_TABLE_SPACING}{users}") # noqa: E501 + leaderboard_rows.append( + f"{rank}{' ' * LEADERBOARD_TABLE_SPACING}{name}{' ' * LEADERBOARD_TABLE_SPACING}{users}" + ) rank_spacing = " " * (rank_len + LEADERBOARD_TABLE_SPACING) name_spacing = " " * (name_len + LEADERBOARD_TABLE_SPACING - len(name_field)) diff --git a/general/betheprofessional/models.py b/general/betheprofessional/models.py index b3bce618f..729ab5f33 100644 --- a/general/betheprofessional/models.py +++ b/general/betheprofessional/models.py @@ -14,10 +14,10 @@ class BTPTopic(Base): @staticmethod async def create( - name: str, - role_id: Union[int, None], - assignable: bool, - parent: Optional[Union[int, None]], + name: str, + role_id: Union[int, None], + assignable: bool, + parent: Optional[Union[int, None]], ) -> "BTPTopic": row = BTPTopic(name=name, role_id=role_id, parent=parent, assignable=assignable) await db.add(row) From 16dd9df1681e417d01dccd3f2829845ef747f0d2 Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Sun, 9 Jan 2022 14:27:30 +0100 Subject: [PATCH 40/68] Trailing Comma --- general/betheprofessional/cog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/general/betheprofessional/cog.py b/general/betheprofessional/cog.py index 6b52123fd..9c4c2f1e4 100644 --- a/general/betheprofessional/cog.py +++ b/general/betheprofessional/cog.py @@ -379,7 +379,7 @@ async def leaderboard(self, ctx: Context, n: Optional[int] = None, use_cache: bo name: str = topic.name.ljust(name_len, " ") rank: str = "#" + str(i + 1).rjust(rank_len - 1, "0") leaderboard_rows.append( - f"{rank}{' ' * LEADERBOARD_TABLE_SPACING}{name}{' ' * LEADERBOARD_TABLE_SPACING}{users}" + f"{rank}{' ' * LEADERBOARD_TABLE_SPACING}{name}{' ' * LEADERBOARD_TABLE_SPACING}{users}", ) rank_spacing = " " * (rank_len + LEADERBOARD_TABLE_SPACING) From 8c9c3c7e5454499b287d4f708cabee6debb42705 Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Sun, 9 Jan 2022 15:11:42 +0100 Subject: [PATCH 41/68] Added Docs --- general/betheprofessional/documentation.md | 95 +++++++++++++++++----- 1 file changed, 73 insertions(+), 22 deletions(-) diff --git a/general/betheprofessional/documentation.md b/general/betheprofessional/documentation.md index a954d3b7f..878378d8d 100644 --- a/general/betheprofessional/documentation.md +++ b/general/betheprofessional/documentation.md @@ -1,41 +1,53 @@ # BeTheProfessional -This cog contains a system for self-assignable roles (further referred to as `topics`). +This cog contains a system for self-assignable topics ## `list_topics` -The `.?` command lists all available topics. +The `.?` command lists all available topics at the level `parent_topic`. + +By default `parent_topic` is the Root Level. ```css -.? +.? [parent_topic] ``` +| Argument | Required | Description | +|:--------------:|:--------:|:-----------------------| +| `parent_topic` | | Parent Level of Topics | ## `assign_topics` The `.+` command assigns the user the specified topics. + +!!! important + Use only the topic name! Not the Path! + ```css .+ ``` -|Argument|Required|Description| -|:------:|:------:|:----------| -|`topic`|:fontawesome-solid-check:|A topic. Multible topics can be added by separating them using `,` or `;`| +| Argument | Required | Description | +|:--------:|:-------------------------:|:-------------------------------------------------------------------------------| +| `topic` | :fontawesome-solid-check: | A topic name. Multible topics can be added by separating them using `,` or `;` | ## `unassign_topics` The `.-` command unassigns the user the specified topics. +!!! important + Use only the topic name! Not the Path! + ```css .- ``` -|Argument|Required|Description| -|:------:|:------:|:----------| -|`topic`|:fontawesome-solid-check:|A topic. Multible topics can be removed by separating them using `,` or `;`.| +| Argument | Required | Description | +|:--------:|:-------------------------:|:----------------------------------------------------------------------------------| +| `topic` | :fontawesome-solid-check: | A topic name. Multible topics can be removed by separating them using `,` or `;`. | !!! note You can use `.- *` to remove all topics at once. @@ -45,36 +57,75 @@ The `.-` command unassigns the user the specified topics. The `.*` command adds new topics to the list of available topics. +!!! note + You can use a topic's path! + +Topic Path Examples: + - `Parent/Child` - Parent must already exist + - `TopLevelNode` + - `Main/Parent/Child2` - Main and Parent must already exist + ```css .* ``` -|Argument|Required|Description| -|:------:|:------:|:----------| -|`topic`|:fontawesome-solid-check:|The new topic's name. If no role with this name (case insensitive) exists, one is created. Multible topics can be registered by separating them using `,` or `;`.| - +| Argument | Required | Description | +|:------------:|:-------------------------:|:---------------------------------------------------------------------------------------------| +| `topic` | :fontawesome-solid-check: | The new topic's path. Multible topics can be registered by separating them using `,` or `;`. | +| `assignable` | | Asignability of the created topic/topics | ## `delete_topics` The `./` command removes topics from the list of available topics and deletes the associated roles. +!!! important + Use only the topic name! Not the Path! + ```css ./ ``` -|Argument|Required|Description| -|:------:|:------:|:----------| -|`topic`|:fontawesome-solid-check:|A topic. Multible topics can be deleted by separating them using `,` or `;`.| +| Argument | Required | Description | +|:--------:|:-------------------------:|:----------------------------------------------------------------------------------| +| `topic` | :fontawesome-solid-check: | A topic name. Multible topics can be deleted by separating them using `,` or `;`. | -## `unregister_topics` +## `topic` +The `.topic` command pings all members by topic name. +If a role exists for the topic, it'll ping the role. -The `.%` command unregisters topics without deleting the associated roles. +If `message` is set, the bot will reply to the given message. ```css -.% +.topic [message] ``` -|Argument|Required|Description| -|:------:|:------:|:----------| -|`topic`|:fontawesome-solid-check:|A topic. Multible topics can be unregistered by separating them using `,` or `;`.| +| Argument | Required | Description | +|:------------:|:-------------------------:|:---------------------------------------------------| +| `topic_name` | :fontawesome-solid-check: | A topic name. | +| `message` | | A Discord Message. e.g. Message ID or Message Link | + +## `btp` +BeTheProfessional Command Group + +### `leaderboard` +The `.btp leaderboard` command lists the top `n` topics sorted by users. + +```css +.btp [leaderboard|lb] [n] +``` + +| Argument | Required | Description | +|:-----------:|:--------:|:-----------------------------------------------------------------------------------------------------------------------------------------------| +| `n` | | Number of topics shown in the leaderboard. Limited by a Setting. Permission to bypass the Limit `betheprofessional.bypass_leaderboard_n_limit` | +| `use_cache` | | Disable Cache. Requires the Bypass Permission `betheprofessional.bypass_leaderboard_cache` | + +## `topic_update_roles` +The `.topic_update_roles` manually updates the Top Topics. +The Top Topics will get a Role. +These roles remain even in the case of a rejoin. +It will usually get executed in a 24-hour loop. + +```css +.[topic_update_roles|topic_update|update_roles] +``` \ No newline at end of file From 9e01906b12bf132851bd02b20c2432d200a206a6 Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Sun, 9 Jan 2022 15:17:54 +0100 Subject: [PATCH 42/68] Fixed MD Style --- general/betheprofessional/documentation.md | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/general/betheprofessional/documentation.md b/general/betheprofessional/documentation.md index 878378d8d..99aafbc6f 100644 --- a/general/betheprofessional/documentation.md +++ b/general/betheprofessional/documentation.md @@ -17,6 +17,7 @@ By default `parent_topic` is the Root Level. |:--------------:|:--------:|:-----------------------| | `parent_topic` | | Parent Level of Topics | + ## `assign_topics` The `.+` command assigns the user the specified topics. @@ -61,9 +62,10 @@ The `.*` command adds new topics to the list of available topics. You can use a topic's path! Topic Path Examples: - - `Parent/Child` - Parent must already exist - - `TopLevelNode` - - `Main/Parent/Child2` - Main and Parent must already exist + +- `Parent/Child` - Parent must already exist +- `TopLevelNode` +- `Main/Parent/Child2` - Main and Parent must already exist ```css .* @@ -74,6 +76,7 @@ Topic Path Examples: | `topic` | :fontawesome-solid-check: | The new topic's path. Multible topics can be registered by separating them using `,` or `;`. | | `assignable` | | Asignability of the created topic/topics | + ## `delete_topics` The `./` command removes topics from the list of available topics and deletes the associated roles. @@ -91,6 +94,7 @@ The `./` command removes topics from the list of available topics and deletes th ## `topic` + The `.topic` command pings all members by topic name. If a role exists for the topic, it'll ping the role. @@ -105,10 +109,14 @@ If `message` is set, the bot will reply to the given message. | `topic_name` | :fontawesome-solid-check: | A topic name. | | `message` | | A Discord Message. e.g. Message ID or Message Link | + ## `btp` + BeTheProfessional Command Group + ### `leaderboard` + The `.btp leaderboard` command lists the top `n` topics sorted by users. ```css @@ -120,7 +128,9 @@ The `.btp leaderboard` command lists the top `n` topics sorted by users. | `n` | | Number of topics shown in the leaderboard. Limited by a Setting. Permission to bypass the Limit `betheprofessional.bypass_leaderboard_n_limit` | | `use_cache` | | Disable Cache. Requires the Bypass Permission `betheprofessional.bypass_leaderboard_cache` | + ## `topic_update_roles` + The `.topic_update_roles` manually updates the Top Topics. The Top Topics will get a Role. These roles remain even in the case of a rejoin. @@ -128,4 +138,4 @@ It will usually get executed in a 24-hour loop. ```css .[topic_update_roles|topic_update|update_roles] -``` \ No newline at end of file +``` From 64bb3f6e0cda357a88e1ba1ffa1407eb2c91a64a Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Sun, 9 Jan 2022 17:30:49 +0100 Subject: [PATCH 43/68] Fixed Role Update Loop + Reload Command, Added BTP Main Command, Added BTP Commands to change the Settings --- general/betheprofessional/cog.py | 85 +++++++++++++++++-- general/betheprofessional/translations/en.yml | 23 ++++- 2 files changed, 101 insertions(+), 7 deletions(-) diff --git a/general/betheprofessional/cog.py b/general/betheprofessional/cog.py index 9c4c2f1e4..db93d15e9 100644 --- a/general/betheprofessional/cog.py +++ b/general/betheprofessional/cog.py @@ -101,7 +101,11 @@ class BeTheProfessionalCog(Cog, name="BeTheProfessional"): ] async def on_ready(self): - self.update_roles.start() + self.update_roles.cancel() + try: + self.update_roles.start() + except RuntimeError: + self.update_roles.restart() @commands.command(name="?") @guild_only() @@ -113,7 +117,7 @@ async def list_topics(self, ctx: Context, parent_topic: Optional[str]): None if parent_topic is None else await db.first(select(BTPTopic).filter_by(name=parent_topic)) - or CommandError(t.topic_not_found(parent_topic)) # noqa: W503 + or CommandError(t.topic_not_found(parent_topic)) # noqa: W503 ) if isinstance(parent, CommandError): raise parent @@ -167,7 +171,7 @@ async def assign_topics(self, ctx: Context, *, topics: str): topic for topic in await parse_topics(topics) if (await db.exists(select(BTPTopic).filter_by(id=topic.id))) - and not (await db.exists(select(BTPUser).filter_by(user_id=member.id, topic=topic.id))) # noqa: W503 + and not (await db.exists(select(BTPUser).filter_by(user_id=member.id, topic=topic.id))) # noqa: W503 ] roles: List[Role] = [] @@ -278,7 +282,7 @@ async def delete_topics(self, ctx: Context, *, topics: str): btp_topic = await db.first(select(BTPTopic).filter_by(name=topic)) delete_topics.append(btp_topic) for child_topic in await db.all( - select(BTPTopic).filter_by(parent=btp_topic.id), + select(BTPTopic).filter_by(parent=btp_topic.id), ): # TODO Recursive? Fix more level childs delete_topics.insert(0, child_topic) for topic in delete_topics: @@ -326,8 +330,77 @@ async def topic(self, ctx: Context, topic_name: str, message: Optional[Message]) @commands.group() @guild_only() async def btp(self, ctx: Context): - if ctx.invoked_subcommand is None: - raise UserInputError + if ctx.subcommand_passed is not None: + if ctx.invoked_subcommand is None: + raise UserInputError + return + embed = Embed(title=t.betheprofessional, color=Colors.BeTheProfessional) + for setting_item in t.settings.__dict__["_fallback"].keys(): + data = getattr(t.settings, setting_item) + embed.add_field( + name=data.name, + value=await getattr(BeTheProfessionalSettings, data.internal_name).get(), + inline=False, + ) + await reply(ctx, embed=embed) + + async def change_setting(self, ctx: Context, name: str, value: any): + data = getattr(t.settings, name) + await getattr(BeTheProfessionalSettings, data.internal_name).set(value) + + embed = Embed(title=t.betheprofessional, color=Colors.green) + embed.description = data.updated(value) + + await reply(ctx, embed=embed) + await send_to_changelog(ctx.guild, embed.description) + + @btp.command() + @guild_only() + @BeTheProfessionalPermission.manage.check + async def role_limit(self, ctx: Context, role_limit: int): + """ + changes the btp role limit + """ + + if role_limit <= 0: + raise CommandError(t.must_be_above_zero(t.settings.role_limit.name)) + await self.change_setting(ctx, "role_limit", role_limit) + + @btp.command() + @guild_only() + @BeTheProfessionalPermission.manage.check + async def role_create_min_users(self, ctx: Context, role_create_min_users: int): + """ + changes the btp role create min users count + """ + + if role_create_min_users < 0: + raise CommandError(t.must_be_zero_or_above(t.settings.role_create_min_users.name)) + await self.change_setting(ctx, "role_create_min_users", role_create_min_users) + + @btp.command() + @guild_only() + @BeTheProfessionalPermission.manage.check + async def leaderboard_default_n(self, ctx: Context, leaderboard_default_n: int): + """ + changes the btp leaderboard default n + """ + + if leaderboard_default_n <= 0: + raise CommandError(t.must_be_above_zero(t.settings.leaderboard_default_n.name)) + await self.change_setting(ctx, "leaderboard_default_n", leaderboard_default_n) + + @btp.command() + @guild_only() + @BeTheProfessionalPermission.manage.check + async def leaderboard_max_n(self, ctx: Context, leaderboard_max_n: int): + """ + changes the btp leaderboard max n + """ + + if leaderboard_max_n <= 0: + raise CommandError(t.must_be_above_zero(t.settings.leaderboard_max_n.name)) + await self.change_setting(ctx, "leaderboard_max_n", leaderboard_max_n) @btp.command(aliases=["lb"]) @guild_only() diff --git a/general/betheprofessional/translations/en.yml b/general/betheprofessional/translations/en.yml index 92db00614..e5d1a257e 100644 --- a/general/betheprofessional/translations/en.yml +++ b/general/betheprofessional/translations/en.yml @@ -56,4 +56,25 @@ fetching_topic_role_failed: "Failed to fetch Role of the Topic `{}` with the Rol leaderboard_colmn_name: "NAME" leaderboard_colmn_users: "USERS" -leaderboard_title: "Top `{}` - Most assigned Topics" \ No newline at end of file +leaderboard_title: "Top `{}` - Most assigned Topics" + +must_be_above_zero: "{} must be above zero!" +must_be_zero_or_above: "{} must be zero or above!" + +settings: + role_limit: + name: "Role Limit" + internal_name: "RoleLimit" + updated: "The BTP Role Limit is now `{}`" + role_create_min_users: + name: "Role Create Min Users" + internal_name: "RoleCreateMinUsers" + updated: "Role Create Min Users Limit is now `{}`" + leaderboard_default_n: + name: "Leaderboard Default N" + internal_name: "LeaderboardDefaultN" + updated: "Leaderboard Default N is now `{}`" + leaderboard_max_n: + name: "Leaderboard Max N" + internal_name: "LeaderboardMaxN" + updated: "Leaderboard Max N is now `{}`" \ No newline at end of file From 1254e49c0ffd0e4f76e4dd513caa47273587277e Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Sun, 9 Jan 2022 17:42:20 +0100 Subject: [PATCH 44/68] Added read Permission and Permission Check, Added new commands to the docs --- general/betheprofessional/cog.py | 1 + general/betheprofessional/documentation.md | 54 ++++++++++++++++++- general/betheprofessional/permissions.py | 1 + general/betheprofessional/translations/en.yml | 3 +- 4 files changed, 57 insertions(+), 2 deletions(-) diff --git a/general/betheprofessional/cog.py b/general/betheprofessional/cog.py index db93d15e9..7e529c877 100644 --- a/general/betheprofessional/cog.py +++ b/general/betheprofessional/cog.py @@ -329,6 +329,7 @@ async def topic(self, ctx: Context, topic_name: str, message: Optional[Message]) @commands.group() @guild_only() + @BeTheProfessionalPermission.read.check async def btp(self, ctx: Context): if ctx.subcommand_passed is not None: if ctx.invoked_subcommand is None: diff --git a/general/betheprofessional/documentation.md b/general/betheprofessional/documentation.md index 99aafbc6f..e2567dc11 100644 --- a/general/betheprofessional/documentation.md +++ b/general/betheprofessional/documentation.md @@ -112,7 +112,8 @@ If `message` is set, the bot will reply to the given message. ## `btp` -BeTheProfessional Command Group +The `.btp` command shows all BTP Settings. +It requires the `betheprofessional.read` Permission. ### `leaderboard` @@ -129,6 +130,57 @@ The `.btp leaderboard` command lists the top `n` topics sorted by users. | `use_cache` | | Disable Cache. Requires the Bypass Permission `betheprofessional.bypass_leaderboard_cache` | +### `role_limit` + +The `.btp role_limit` command is used to change the `role_limit` Settings for BTP. + +```css +.btp role_limit +``` + +| Argument | Required | Description | +|:------------:|:-------------------------:|:----------------------------| +| `role_limit` | :fontawesome-solid-check: | New value of `role_setting` | + + +### `role_create_min_users` + +The `.btp role_create_min_users` command is used to change the `role_create_min_users` Settings for BTP. + +```css +.btp role_create_min_users +``` + +| Argument | Required | Description | +|:-----------------------:|:-------------------------:|:-------------------------------------| +| `role_create_min_users` | :fontawesome-solid-check: | New value of `role_create_min_users` | + + +### `leaderboard_default_n` + +The `.btp leaderboard_default_n` command is used to change the `leaderboard_default_n` Settings for BTP. + +```css +.btp leaderboard_default_n +``` + +| Argument | Required | Description | +|:-----------------------:|:-------------------------:|:-------------------------------------| +| `leaderboard_default_n` | :fontawesome-solid-check: | New value of `leaderboard_default_n` | + + +### `leaderboard_max_n` + +The `.btp leaderboard_max_n` command is used to change the `leaderboard_max_n` Settings for BTP. + +```css +.btp leaderboard_max_n +``` + +| Argument | Required | Description | +|:-------------------:|:-------------------------:|:---------------------------------| +| `leaderboard_max_n` | :fontawesome-solid-check: | New value of `leaderboard_max_n` | + ## `topic_update_roles` The `.topic_update_roles` manually updates the Top Topics. diff --git a/general/betheprofessional/permissions.py b/general/betheprofessional/permissions.py index e6acf68bb..605dbae52 100644 --- a/general/betheprofessional/permissions.py +++ b/general/betheprofessional/permissions.py @@ -10,5 +10,6 @@ def description(self) -> str: return t.betheprofessional.permissions[self.name] manage = auto() + read = auto() bypass_leaderboard_cache = auto() bypass_leaderboard_n_limit = auto() diff --git a/general/betheprofessional/translations/en.yml b/general/betheprofessional/translations/en.yml index e5d1a257e..8a47fce0b 100644 --- a/general/betheprofessional/translations/en.yml +++ b/general/betheprofessional/translations/en.yml @@ -1,5 +1,6 @@ permissions: - manage: manage betheprofessional roles + manage: manage betheprofessional + read: read betheprofessional settings bypass_leaderboard_cache: bypass leadboard cache bypass_leaderboard_n_limit: bypass leaderboard n limit missing_cache_bypass_permission: "Missing Cache bypass Permission" From 45666b6b654d9f13600d6325f40f020f18db8bea Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Sun, 9 Jan 2022 18:00:13 +0100 Subject: [PATCH 45/68] Fixed recursive Topic delition --- general/betheprofessional/cog.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/general/betheprofessional/cog.py b/general/betheprofessional/cog.py index 7e529c877..f03e76d75 100644 --- a/general/betheprofessional/cog.py +++ b/general/betheprofessional/cog.py @@ -279,12 +279,19 @@ async def delete_topics(self, ctx: Context, *, topics: str): if not await db.exists(select(BTPTopic).filter_by(name=topic)): raise CommandError(t.topic_not_registered(topic)) else: - btp_topic = await db.first(select(BTPTopic).filter_by(name=topic)) + btp_topic: BTPTopic = await db.first(select(BTPTopic).filter_by(name=topic)) + delete_topics.append(btp_topic) - for child_topic in await db.all( - select(BTPTopic).filter_by(parent=btp_topic.id), - ): # TODO Recursive? Fix more level childs - delete_topics.insert(0, child_topic) + + queue: list[int] = [btp_topic.id] + + while len(queue) != 0: + topic_id = queue.pop() + for child_topic in await db.all( + select(BTPTopic).filter_by(parent=topic_id), + ): + delete_topics.insert(0, child_topic) + queue.append(child_topic.id) for topic in delete_topics: if topic.role_id is not None: role: Role = ctx.guild.get_role(topic.role_id) From 3b659967312828c815ff4c3602136dd80fd19c44 Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Sun, 9 Jan 2022 21:11:34 +0100 Subject: [PATCH 46/68] Implemented Signle (Un)Assign Help --- general/betheprofessional/cog.py | 30 ++++++++++++++++++- general/betheprofessional/translations/en.yml | 2 ++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/general/betheprofessional/cog.py b/general/betheprofessional/cog.py index f03e76d75..7d435ed61 100644 --- a/general/betheprofessional/cog.py +++ b/general/betheprofessional/cog.py @@ -184,8 +184,22 @@ async def assign_topics(self, ctx: Context, *, topics: str): embed = Embed(title=t.betheprofessional, colour=Colors.BeTheProfessional) embed.description = t.topics_added(cnt=len(topics)) - if not topics: + + redis_key: str = f"btp:single_un_assign:{ctx.author.id}" + + if len(topics) == 0: embed.colour = Colors.error + elif len(topics) == 1: + count = await redis.incr(redis_key) + await redis.expire(redis_key, 30) + + if count > 3: + await reply(ctx, embed=embed) + + embed.colour = Colors.BeTheProfessional + embed.description = t.single_un_assign_help + else: + await redis.delete(redis_key) await reply(ctx, embed=embed) @@ -216,6 +230,20 @@ async def unassign_topics(self, ctx: Context, *, topics: str): embed = Embed(title=t.betheprofessional, colour=Colors.BeTheProfessional) embed.description = t.topics_removed(cnt=len(affected_topics)) + + redis_key: str = f"btp:single_un_assign:{ctx.author.id}" + + if len(affected_topics) == 1: + count = await redis.incr(redis_key) + await redis.expire(redis_key, 30) + + if count > 3: + await reply(ctx, embed=embed) + + embed.description = t.single_un_assign_help + elif len(affected_topics) > 1: + await redis.delete(redis_key) + await reply(ctx, embed=embed) @commands.command(name="*") diff --git a/general/betheprofessional/translations/en.yml b/general/betheprofessional/translations/en.yml index 8a47fce0b..de0b88030 100644 --- a/general/betheprofessional/translations/en.yml +++ b/general/betheprofessional/translations/en.yml @@ -43,6 +43,8 @@ log_topics_unregistered: one: "The **topic** {topics} has been **removed**." many: "{cnt} **topics** have been **removed**: {topics}" +single_un_assign_help: "Hey, did you know, that you can assign multiple topics by using `.+ TOPIC1,TOPIC2,TOPIC3` and remove multiple topics the same way using `.-`?\nTo remove all your Topics you can use `.- *`" + parent_not_exists: "Parent `{}` doesn't exists" parent_format_help: "Please write `[Parents/]Topic-Name`" group_not_parent_group: "The group `{}` is not the same as the group of the Parent `{}`" From 7aac1c98b1b85a34f77fd972322202d254cca766 Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Sun, 9 Jan 2022 21:12:16 +0100 Subject: [PATCH 47/68] Added reset for redis counter --- general/betheprofessional/cog.py | 1 + 1 file changed, 1 insertion(+) diff --git a/general/betheprofessional/cog.py b/general/betheprofessional/cog.py index 7d435ed61..50a5ec04b 100644 --- a/general/betheprofessional/cog.py +++ b/general/betheprofessional/cog.py @@ -238,6 +238,7 @@ async def unassign_topics(self, ctx: Context, *, topics: str): await redis.expire(redis_key, 30) if count > 3: + await redis.delete(redis_key) await reply(ctx, embed=embed) embed.description = t.single_un_assign_help From 9c180063aa3d8b6c8fc5e9379c3aac2bbdbe8f4e Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Sun, 9 Jan 2022 21:42:23 +0100 Subject: [PATCH 48/68] Added missing awaits, Added usertopic command --- general/betheprofessional/cog.py | 35 +++++++++++++++++-- general/betheprofessional/translations/en.yml | 5 +++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/general/betheprofessional/cog.py b/general/betheprofessional/cog.py index 50a5ec04b..8fcb8f4f4 100644 --- a/general/betheprofessional/cog.py +++ b/general/betheprofessional/cog.py @@ -453,7 +453,7 @@ async def leaderboard(self, ctx: Context, n: Optional[int] = None, use_cache: bo if default_n > max_n: await send_alert(ctx.guild, t.leaderboard_default_n_bigger_than_max_n) raise CommandError(t.leaderboard_configuration_error) - if n > max_n and not BeTheProfessionalPermission.bypass_leaderboard_n_limit.check_permissions(ctx.author): + if n > max_n and not await BeTheProfessionalPermission.bypass_leaderboard_n_limit.check_permissions(ctx.author): raise CommandError(t.leaderboard_n_too_big(n, max_n)) if n <= 0: raise CommandError(t.leaderboard_n_zero_error) @@ -461,7 +461,7 @@ async def leaderboard(self, ctx: Context, n: Optional[int] = None, use_cache: bo cached_leaderboard: Optional[str] = None if use_cache: - if not BeTheProfessionalPermission.bypass_leaderboard_cache.check_permissions(ctx.author): + if not await BeTheProfessionalPermission.bypass_leaderboard_cache.check_permissions(ctx.author): raise CommandError(t.missing_cache_bypass_permission) cached_leaderboard = await redis.get(f"btp:leaderboard:n:{n}") @@ -503,7 +503,7 @@ async def leaderboard(self, ctx: Context, n: Optional[int] = None, use_cache: bo embed = Embed(title=t.leaderboard_title(n), description=f"```css\n{leaderboard}\n```") - if len(embed.description) > PyDrocsid.embeds.EmbedLimits.DESCRIPTION or True: + if len(embed.description) > PyDrocsid.embeds.EmbedLimits.DESCRIPTION: embed.description = None with io.StringIO() as leaderboard_file: leaderboard_file.write(leaderboard) @@ -513,6 +513,35 @@ async def leaderboard(self, ctx: Context, n: Optional[int] = None, use_cache: bo else: await reply(ctx, embed=embed) + @commands.command(name="usertopics", aliases=["usertopic", "utopics", "utopic"]) + async def user_topics(self, ctx: Context, member: Optional[Member]): + """ + lists all topics of a member + """ + + if member is None: + member = ctx.author + + topics_assigns: list[BTPUser] = await db.all(select(BTPUser).filter_by(user_id=member.id)) + topics: list[BTPTopic] = [ + await db.first(select(BTPTopic).filter_by(id=assignment.topic)) for assignment in topics_assigns + ] + + embed = Embed(title=t.betheprofessional, color=Colors.BeTheProfessional) + + embed.set_author(name=str(member), icon_url=member.display_avatar.url) + + topics_str: str = "" + + if len(topics_assigns) == 0: + embed.colour = Colors.red + else: + topics_str = ', '.join([f"`{topic.name}`" for topic in topics]) + + embed.description = t.user_topics(member.mention, topics_str, cnt=len(topics)) + + await reply(ctx, embed=embed) + @commands.command(aliases=["topic_update", "update_roles"]) @guild_only() @BeTheProfessionalPermission.manage.check diff --git a/general/betheprofessional/translations/en.yml b/general/betheprofessional/translations/en.yml index de0b88030..392c7c4c6 100644 --- a/general/betheprofessional/translations/en.yml +++ b/general/betheprofessional/translations/en.yml @@ -45,6 +45,11 @@ log_topics_unregistered: single_un_assign_help: "Hey, did you know, that you can assign multiple topics by using `.+ TOPIC1,TOPIC2,TOPIC3` and remove multiple topics the same way using `.-`?\nTo remove all your Topics you can use `.- *`" +user_topics: + zero: "{} has no topics assigned" + one: "{} has assigned the following topic: {}" + many: "{} has assigned the following topics: {}" + parent_not_exists: "Parent `{}` doesn't exists" parent_format_help: "Please write `[Parents/]Topic-Name`" group_not_parent_group: "The group `{}` is not the same as the group of the Parent `{}`" From 0718aa4405cf1271d37071380c204455862b886d Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Sun, 9 Jan 2022 21:45:23 +0100 Subject: [PATCH 49/68] Added usertopics command docs --- general/betheprofessional/documentation.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/general/betheprofessional/documentation.md b/general/betheprofessional/documentation.md index e2567dc11..933ae2268 100644 --- a/general/betheprofessional/documentation.md +++ b/general/betheprofessional/documentation.md @@ -181,6 +181,20 @@ The `.btp leaderboard_max_n` command is used to change the `leaderboard_max_n` S |:-------------------:|:-------------------------:|:---------------------------------| | `leaderboard_max_n` | :fontawesome-solid-check: | New value of `leaderboard_max_n` | + +## `user_topics` + +The `usertopics` command is used to show all topics a User has assigned. + +```css +.[usertopics|usertopic|utopics|utopic] [member] +``` + +| Argument | Required | Description | +|:--------:|:--------:|:------------------------------------------------------| +| `member` | | A member. Default is the Member executing the command | + + ## `topic_update_roles` The `.topic_update_roles` manually updates the Top Topics. From 4562c424922fe1c81a2e338e9b40ae0840d4e6c9 Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Sun, 9 Jan 2022 21:50:35 +0100 Subject: [PATCH 50/68] Reformatted with black --- general/betheprofessional/cog.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/general/betheprofessional/cog.py b/general/betheprofessional/cog.py index 8fcb8f4f4..5cf6e4d20 100644 --- a/general/betheprofessional/cog.py +++ b/general/betheprofessional/cog.py @@ -117,7 +117,7 @@ async def list_topics(self, ctx: Context, parent_topic: Optional[str]): None if parent_topic is None else await db.first(select(BTPTopic).filter_by(name=parent_topic)) - or CommandError(t.topic_not_found(parent_topic)) # noqa: W503 + or CommandError(t.topic_not_found(parent_topic)) # noqa: W503 ) if isinstance(parent, CommandError): raise parent @@ -171,7 +171,7 @@ async def assign_topics(self, ctx: Context, *, topics: str): topic for topic in await parse_topics(topics) if (await db.exists(select(BTPTopic).filter_by(id=topic.id))) - and not (await db.exists(select(BTPUser).filter_by(user_id=member.id, topic=topic.id))) # noqa: W503 + and not (await db.exists(select(BTPUser).filter_by(user_id=member.id, topic=topic.id))) # noqa: W503 ] roles: List[Role] = [] @@ -317,7 +317,7 @@ async def delete_topics(self, ctx: Context, *, topics: str): while len(queue) != 0: topic_id = queue.pop() for child_topic in await db.all( - select(BTPTopic).filter_by(parent=topic_id), + select(BTPTopic).filter_by(parent=topic_id), ): delete_topics.insert(0, child_topic) queue.append(child_topic.id) @@ -536,7 +536,7 @@ async def user_topics(self, ctx: Context, member: Optional[Member]): if len(topics_assigns) == 0: embed.colour = Colors.red else: - topics_str = ', '.join([f"`{topic.name}`" for topic in topics]) + topics_str = ", ".join([f"`{topic.name}`" for topic in topics]) embed.description = t.user_topics(member.mention, topics_str, cnt=len(topics)) From ffeb2989c5e02bbec70850937c9425d60008c352 Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Sun, 9 Jan 2022 21:50:35 +0100 Subject: [PATCH 51/68] Added Pagigantion and improved LB --- general/betheprofessional/cog.py | 55 ++++++++++--------- general/betheprofessional/translations/en.yml | 4 +- 2 files changed, 32 insertions(+), 27 deletions(-) diff --git a/general/betheprofessional/cog.py b/general/betheprofessional/cog.py index 5cf6e4d20..2928c68f8 100644 --- a/general/betheprofessional/cog.py +++ b/general/betheprofessional/cog.py @@ -458,14 +458,16 @@ async def leaderboard(self, ctx: Context, n: Optional[int] = None, use_cache: bo if n <= 0: raise CommandError(t.leaderboard_n_zero_error) - cached_leaderboard: Optional[str] = None + cached_leaderboard_parts: Optional[list[str]] = None + redis_key = f"btp:leaderboard:n:{n}" if use_cache: if not await BeTheProfessionalPermission.bypass_leaderboard_cache.check_permissions(ctx.author): raise CommandError(t.missing_cache_bypass_permission) - cached_leaderboard = await redis.get(f"btp:leaderboard:n:{n}") + cached_leaderboard_parts = await redis.lrange(redis_key, 0, await redis.llen(redis_key)) - if cached_leaderboard is None: + leaderboard_parts: list[str] = [] + if not cached_leaderboard_parts: topic_count: Dict[int, int] = {} for topic in await db.all(select(BTPTopic)): @@ -482,36 +484,39 @@ async def leaderboard(self, ctx: Context, n: Optional[int] = None, use_cache: bo rank_len = len(str(len(top_topics))) + 1 name_len = max(max([len(topic.name) for topic in await db.all(select(BTPTopic))]), len(name_field)) - leaderboard_rows: list[str] = [] + rank_spacing = " " * (rank_len + LEADERBOARD_TABLE_SPACING) + name_spacing = " " * (name_len + LEADERBOARD_TABLE_SPACING - len(name_field)) + + header: str = f"{rank_spacing}{name_field}{name_spacing}{users_field}" + + current_part: str = header for i, topic_id in enumerate(top_topics): topic: BTPTopic = await db.first(select(BTPTopic).filter_by(id=topic_id)) users: int = topic_count[topic_id] name: str = topic.name.ljust(name_len, " ") rank: str = "#" + str(i + 1).rjust(rank_len - 1, "0") - leaderboard_rows.append( - f"{rank}{' ' * LEADERBOARD_TABLE_SPACING}{name}{' ' * LEADERBOARD_TABLE_SPACING}{users}", - ) - - rank_spacing = " " * (rank_len + LEADERBOARD_TABLE_SPACING) - name_spacing = " " * (name_len + LEADERBOARD_TABLE_SPACING - len(name_field)) + current_line = f"{rank}{' ' * LEADERBOARD_TABLE_SPACING}{name}{' ' * LEADERBOARD_TABLE_SPACING}{users}" + if current_part == "": + current_part = current_line + else: + if len(current_part + "\n" + current_line) + 9 > PyDrocsid.embeds.EmbedLimits.FIELD_VALUE: + leaderboard_parts.append(current_part) + current_part = current_line + else: + current_part += "\n" + current_line + if current_part != "": + leaderboard_parts.append(current_part) - header: str = f"{rank_spacing}{name_field}{name_spacing}{users_field}\n" - leaderboard: str = header + "\n".join(leaderboard_rows) - await redis.setex(f"btp:leaderboard:n:{n}", CACHE_TTL, leaderboard) + for part in leaderboard_parts: + await redis.lpush(redis_key, part) + await redis.expire(redis_key, CACHE_TTL) else: - leaderboard: str = cached_leaderboard - - embed = Embed(title=t.leaderboard_title(n), description=f"```css\n{leaderboard}\n```") + leaderboard_parts = cached_leaderboard_parts - if len(embed.description) > PyDrocsid.embeds.EmbedLimits.DESCRIPTION: - embed.description = None - with io.StringIO() as leaderboard_file: - leaderboard_file.write(leaderboard) - leaderboard_file.seek(0) - file = File(fp=leaderboard_file, filename="output.css") - await reply(ctx, embed=embed, file=file) - else: - await reply(ctx, embed=embed) + embed = Embed(title=t.leaderboard_title(n)) + for part in leaderboard_parts: + embed.add_field(name="** **", value=f"```css\n{part}\n```", inline=False) + await send_long_embed(ctx, embed, paginate=True) @commands.command(name="usertopics", aliases=["usertopic", "utopics", "utopic"]) async def user_topics(self, ctx: Context, member: Optional[Member]): diff --git a/general/betheprofessional/translations/en.yml b/general/betheprofessional/translations/en.yml index 392c7c4c6..446b79d23 100644 --- a/general/betheprofessional/translations/en.yml +++ b/general/betheprofessional/translations/en.yml @@ -62,8 +62,8 @@ leaderboard_n_zero_error: "N cant be zero or less!" fetching_topic_role_failed: "Failed to fetch Role of the Topic `{}` with the Role ID `{}`" -leaderboard_colmn_name: "NAME" -leaderboard_colmn_users: "USERS" +leaderboard_colmn_name: "[NAME]" +leaderboard_colmn_users: "[USERS]" leaderboard_title: "Top `{}` - Most assigned Topics" must_be_above_zero: "{} must be above zero!" From ae944d920d92c7dcae8a02dda2ff017a4785d75e Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Tue, 1 Mar 2022 09:40:08 +0100 Subject: [PATCH 52/68] fixed documentation code style --- general/betheprofessional/documentation.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/general/betheprofessional/documentation.md b/general/betheprofessional/documentation.md index 7dbe1d887..61eefd883 100644 --- a/general/betheprofessional/documentation.md +++ b/general/betheprofessional/documentation.md @@ -12,6 +12,7 @@ By default `parent_topic` is the Root Level. ```css .? [parent_topic] ``` + Arguments: | Argument | Required | Description | |:--------------:|:--------:|:-----------------------| @@ -29,6 +30,7 @@ The `.+` command assigns the user the specified topics. ```css .+ ``` + Arguments: | Argument | Required | Description | |:--------:|:-------------------------:|:-------------------------------------------------------------------------------| @@ -81,6 +83,7 @@ Arguments: | `assignable` | | Asignability of the created topic/topics | Required Permissions: + - `betheprofessional.manage` @@ -102,8 +105,10 @@ Arguments: | `topic` | :fontawesome-solid-check: | A topic name. Multible topics can be deleted by separating them using `,` or `;`. | Required Permissions: + - `betheprofessional.manage` + ## `%` (unregister topics) The `.%` command unregisters topics without deleting the associated roles. @@ -122,6 +127,7 @@ Required Permissions: - `betheprofessional.manage` + ## `topic` The `.topic` command pings all members by topic name. From c8ef56ffb7381bc0371aa44514afb3506b83e336 Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Tue, 1 Mar 2022 10:20:20 +0100 Subject: [PATCH 53/68] optimized role update loop --- general/betheprofessional/cog.py | 36 ++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/general/betheprofessional/cog.py b/general/betheprofessional/cog.py index 2928c68f8..3f0491719 100644 --- a/general/betheprofessional/cog.py +++ b/general/betheprofessional/cog.py @@ -1,8 +1,8 @@ import io import string -from typing import List, Union, Optional, Dict, Final +from typing import List, Union, Optional, Dict, Final, Set -from discord import Member, Embed, Role, Message, File +from discord import Member, Embed, Role, Message from discord.ext import commands, tasks from discord.ext.commands import guild_only, Context, CommandError, UserInputError @@ -585,21 +585,25 @@ async def update_roles(self): await self.bot.guilds[0].get_role(topic.role_id).delete() topic.role_id = None - # Create new Topic Role and add Role to Users - # TODO Optimize from `LOOP all topics: LOOP all Members: add role` - # to `LOOP all Members with Topic: add all roles` and separate the role creating + # Create new Topic Roles + roles: Dict[int, Role] = {} for top_topic in top_topics: - if (topic := await db.first(select(BTPTopic).filter_by(id=top_topic, role_id=None))) is not None: - topic.role_id = (await self.bot.guilds[0].create_role(name=topic.name)).id - for btp_user in await db.all(select(BTPUser).filter_by(topic=topic.id)): - member = await self.bot.guilds[0].fetch_member(btp_user.user_id) - if not member: - continue - role = self.bot.guilds[0].get_role(topic.role_id) - if role: - await member.add_roles(role, atomic=False) - else: - await send_alert(self.bot.guilds[0], t.fetching_topic_role_failed(topic.name, topic.role_id)) + topic: BTPTopic = await db.first(select(BTPTopic).filter_by(id=top_topic)) + if topic.role_id is None: + role = await self.bot.guilds[0].create_role(name=topic.name) + topic.role_id = role.id + roles[topic.id] = role + # Iterate over all members(with topics) and add the role to them + member_ids: Set[int] = {btp_user.user_id for btp_user in await db.all(select(BTPUser))} + for member_id in member_ids: + member: Member = self.bot.guilds[0].get_member(member_id) + if member is None: + continue + member_roles: List[Role] = [ + roles.get(btp_user.topic) for btp_user in await db.all(select(BTPUser).filter_by(user_id=member_id)) + ] + member_roles = [item for item in member_roles if item is not None] + await member.add_roles(*member_roles, atomic=False) logger.info("Created Top Topic Roles") From a41725306c86f75d991c2a0ae7c6ff4345a746f2 Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Tue, 1 Mar 2022 10:39:45 +0100 Subject: [PATCH 54/68] added second sort to role update loop --- general/betheprofessional/cog.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/general/betheprofessional/cog.py b/general/betheprofessional/cog.py index 3f0491719..1128632c4 100644 --- a/general/betheprofessional/cog.py +++ b/general/betheprofessional/cog.py @@ -1,6 +1,5 @@ -import io import string -from typing import List, Union, Optional, Dict, Final, Set +from typing import List, Union, Optional, Dict, Final, Set, Tuple from discord import Member, Embed, Role, Message from discord.ext import commands, tasks @@ -568,6 +567,9 @@ async def update_roles(self): for topic in await db.all(select(BTPTopic)): topic_count[topic.id] = await db.count(select(BTPUser).filter_by(topic=topic.id)) + # not using dict.items() because of typing + topic_count_items: list[Tuple[int, int]] = list(zip(topic_count.keys(), topic_count.values())) + topic_count = dict(sorted(topic_count_items, key=lambda x: x[0])) # Sort Topics By Count, Keep only Topics with a Count of BeTheProfessionalSettings.RoleCreateMinUsers or above # Limit Roles to BeTheProfessionalSettings.RoleLimit From cf790dfa51708c5236083577fafc8de7c430cefb Mon Sep 17 00:00:00 2001 From: TheCataliasTNT2k <44349750+TheCataliasTNT2k@users.noreply.github.com> Date: Fri, 8 Apr 2022 19:13:07 +0200 Subject: [PATCH 55/68] added TODOs --- general/betheprofessional/cog.py | 47 ++++++++++++++++++- general/betheprofessional/models.py | 18 +++---- general/betheprofessional/settings.py | 1 + general/betheprofessional/translations/en.yml | 16 +++---- 4 files changed, 64 insertions(+), 18 deletions(-) diff --git a/general/betheprofessional/cog.py b/general/betheprofessional/cog.py index 6876d11e2..de5d44a32 100644 --- a/general/betheprofessional/cog.py +++ b/general/betheprofessional/cog.py @@ -1,5 +1,5 @@ import string -from typing import List, Union, Optional, Dict, Final, Set, Tuple +from typing import List, Union, Optional, Dict, Final, Set, Tuple # TODO remove typing lib from discord import Member, Embed, Role, Message from discord.ext import commands, tasks @@ -40,7 +40,9 @@ async def split_parents(topics: List[str], assignable: bool) -> List[tuple[str, topic_tree = topic.split("/") parents: List[Union[BTPTopic, None, CommandError]] = [ + # TODO use filter_by provided by the library await db.first(select(BTPTopic).filter_by(name=topic)) + # TODO use filter_by provided by the library if await db.exists(select(BTPTopic).filter_by(name=topic)) else CommandError(t.parent_not_exists(topic)) for topic in topic_tree[:-1] @@ -64,6 +66,7 @@ async def parse_topics(topics_str: str) -> List[BTPTopic]: raise CommandError(t.no_topics_registered) for topic_name in split_topics(topics_str): + # TODO use filter_by provided by the library topic = await db.first(select(BTPTopic).filter_by(name=topic_name)) if topic is None and len(all_topics) > 0: @@ -115,6 +118,7 @@ async def list_topics(self, ctx: Context, parent_topic: Optional[str]): parent: Union[BTPTopic, None, CommandError] = ( None if parent_topic is None + # TODO use filter_by provided by the library else await db.first(select(BTPTopic).filter_by(name=parent_topic)) or CommandError(t.topic_not_found(parent_topic)) # noqa: W503 ) @@ -123,6 +127,7 @@ async def list_topics(self, ctx: Context, parent_topic: Optional[str]): embed = Embed(title=t.available_topics_header, colour=Colors.BeTheProfessional) sorted_topics: Dict[str, List[str]] = {} + # TODO use filter_by provided by the library topics: List[BTPTopic] = await db.all(select(BTPTopic).filter_by(parent=None if parent is None else parent.id)) if not topics: embed.colour = Colors.error @@ -132,6 +137,7 @@ async def list_topics(self, ctx: Context, parent_topic: Optional[str]): topics.sort(key=lambda btp_topic: btp_topic.name.lower()) root_topic: Union[BTPTopic, None] = ( + # TODO use filter_by provided by the library None if parent_topic is None else await db.first(select(BTPTopic).filter_by(name=parent_topic)) ) for topic in topics: @@ -148,6 +154,7 @@ async def list_topics(self, ctx: Context, parent_topic: Optional[str]): f"`{topic.name}" + ( # noqa: W503 f" ({c})`" + # TODO use filter_by provided by the library if (c := await db.count(select(BTPTopic).filter_by(parent=topic.id))) > 0 else "`" ) @@ -169,7 +176,9 @@ async def assign_topics(self, ctx: Context, *, topics: str): topics: List[BTPTopic] = [ topic for topic in await parse_topics(topics) + # TODO use filter_by provided by the library if (await db.exists(select(BTPTopic).filter_by(id=topic.id))) + # TODO use filter_by provided by the library and not (await db.exists(select(BTPUser).filter_by(user_id=member.id, topic=topic.id))) # noqa: W503 ] @@ -215,12 +224,14 @@ async def unassign_topics(self, ctx: Context, *, topics: str): topics: List[BTPTopic] = await parse_topics(topics) affected_topics: List[BTPTopic] = [] for topic in topics: + # TODO use filter_by provided by the library if await db.exists(select(BTPUser).filter_by(user_id=member.id, topic=topic.id)): affected_topics.append(topic) roles: List[Role] = [] for topic in affected_topics: + # TODO use filter_by provided by the library await db.delete(await db.first(select(BTPUser).filter_by(topic=topic.id))) if topic.role_id: roles.append(ctx.guild.get_role(topic.role_id)) @@ -267,6 +278,7 @@ async def register_topics(self, ctx: Context, *, topic_paths: str, assignable: b if any(c not in valid_chars for c in topic[0]): raise CommandError(t.topic_invalid_chars(topic)) + # TODO use filter_by provided by the library if await db.exists(select(BTPTopic).filter_by(name=topic[0])): raise CommandError(t.topic_already_registered(topic[0])) else: @@ -300,6 +312,7 @@ async def delete_topics(self, ctx: Context, *, topics: str): delete_topics: list[BTPTopic] = [] for topic in topics: + # TODO two selects for the same thing? and use filter_by provided by the library if not await db.exists(select(BTPTopic).filter_by(name=topic)): raise CommandError(t.topic_not_registered(topic)) else: @@ -307,6 +320,7 @@ async def delete_topics(self, ctx: Context, *, topics: str): delete_topics.append(btp_topic) + # TODO use relationships for children queue: list[int] = [btp_topic.id] while len(queue) != 0: @@ -314,12 +328,17 @@ async def delete_topics(self, ctx: Context, *, topics: str): for child_topic in await db.all(select(BTPTopic).filter_by(parent=topic_id)): delete_topics.insert(0, child_topic) queue.append(child_topic.id) + for topic in delete_topics: if topic.role_id is not None: + # TODO what if role is None? role: Role = ctx.guild.get_role(topic.role_id) await role.delete() + # TODO use filter_by provided by the library for user_topic in await db.all(select(BTPUser).filter_by(topic=topic.id)): + # TODO use db.exec await db.delete(user_topic) + # TODO do not commit for each one separately await db.commit() await db.delete(topic) @@ -343,9 +362,10 @@ async def topic(self, ctx: Context, topic_name: str, message: Optional[Message]) if topic is None: raise CommandError(t.topic_not_found(topic_name)) if topic.role_id is not None: - mention = ctx.guild.get_role(topic.role_id).mention + mention = ctx.guild.get_role(topic.role_id).mention # TODO use <@&ID> else: topic_members: List[BTPUser] = await db.all(select(BTPUser).filter_by(topic=topic.id)) + # TODO what if member does not exist? Why don't you use `<@ID>`? members: List[Member] = [ctx.guild.get_member(member.user_id) for member in topic_members] mention = ", ".join(map(lambda m: m.mention, members)) @@ -364,7 +384,9 @@ async def btp(self, ctx: Context): if ctx.invoked_subcommand is None: raise UserInputError return + embed = Embed(title=t.betheprofessional, color=Colors.BeTheProfessional) + # TODO do not do that!!!! for setting_item in t.settings.__dict__["_fallback"].keys(): data = getattr(t.settings, setting_item) embed.add_field( @@ -372,7 +394,9 @@ async def btp(self, ctx: Context): ) await reply(ctx, embed=embed) + # TODO make function, not method, self not used async def change_setting(self, ctx: Context, name: str, value: any): + # TODO use dictionary data = getattr(t.settings, name) await getattr(BeTheProfessionalSettings, data.internal_name).set(value) @@ -391,6 +415,7 @@ async def role_limit(self, ctx: Context, role_limit: int): """ if role_limit <= 0: + # TODO use quotes for the name in the embed raise CommandError(t.must_be_above_zero(t.settings.role_limit.name)) await self.change_setting(ctx, "role_limit", role_limit) @@ -403,6 +428,7 @@ async def role_create_min_users(self, ctx: Context, role_create_min_users: int): """ if role_create_min_users < 0: + # TODO use quotes for the name in the embed raise CommandError(t.must_be_zero_or_above(t.settings.role_create_min_users.name)) await self.change_setting(ctx, "role_create_min_users", role_create_min_users) @@ -415,6 +441,7 @@ async def leaderboard_default_n(self, ctx: Context, leaderboard_default_n: int): """ if leaderboard_default_n <= 0: + # TODO use quotes for the name in the embed raise CommandError(t.must_be_above_zero(t.settings.leaderboard_default_n.name)) await self.change_setting(ctx, "leaderboard_default_n", leaderboard_default_n) @@ -427,11 +454,13 @@ async def leaderboard_max_n(self, ctx: Context, leaderboard_max_n: int): """ if leaderboard_max_n <= 0: + # TODO use quotes for the name in the embed raise CommandError(t.must_be_above_zero(t.settings.leaderboard_max_n.name)) await self.change_setting(ctx, "leaderboard_max_n", leaderboard_max_n) @btp.command(aliases=["lb"]) @guild_only() + # TODO parameters async def leaderboard(self, ctx: Context, n: Optional[int] = None, use_cache: bool = True): """ lists the top n topics @@ -518,6 +547,7 @@ async def user_topics(self, ctx: Context, member: Optional[Member]): if member is None: member = ctx.author + # TODO use relationships and join topics_assigns: list[BTPUser] = await db.all(select(BTPUser).filter_by(user_id=member.id)) topics: list[BTPTopic] = [ await db.first(select(BTPTopic).filter_by(id=assignment.topic)) for assignment in topics_assigns @@ -557,9 +587,13 @@ async def update_roles(self): logger.info("Started Update Role Loop") topic_count: Dict[int, int] = {} + # TODO rewrite from here.... for topic in await db.all(select(BTPTopic)): + # TODO use relationship and join topic_count[topic.id] = await db.count(select(BTPUser).filter_by(topic=topic.id)) # not using dict.items() because of typing + # TODO Let db sort topics by count and then by + # TODO fix TODO ^^ topic_count_items: list[Tuple[int, int]] = list(zip(topic_count.keys(), topic_count.values())) topic_count = dict(sorted(topic_count_items, key=lambda x: x[0])) @@ -572,8 +606,12 @@ async def update_roles(self): ) )[: await BeTheProfessionalSettings.RoleLimit.get()] + # TODO until here + # Delete old Top Topic Roles + # TODO use filter_by for topic in await db.all(select(BTPTopic).filter(BTPTopic.role_id is not None)): # type: BTPTopic + # TODO use sql "NOT IN" expression if topic.id not in top_topics: if topic.role_id is not None: await self.bot.guilds[0].get_role(topic.role_id).delete() @@ -581,13 +619,16 @@ async def update_roles(self): # Create new Topic Roles roles: Dict[int, Role] = {} + # TODO use sql "IN" expression for top_topic in top_topics: topic: BTPTopic = await db.first(select(BTPTopic).filter_by(id=top_topic)) if topic.role_id is None: role = await self.bot.guilds[0].create_role(name=topic.name) topic.role_id = role.id roles[topic.id] = role + # Iterate over all members(with topics) and add the role to them + # TODO add filter, only select topics with newly added roles member_ids: Set[int] = {btp_user.user_id for btp_user in await db.all(select(BTPUser))} for member_id in member_ids: member: Member = self.bot.guilds[0].get_member(member_id) @@ -596,12 +637,14 @@ async def update_roles(self): member_roles: List[Role] = [ roles.get(btp_user.topic) for btp_user in await db.all(select(BTPUser).filter_by(user_id=member_id)) ] + # TODO use filter or something? member_roles = [item for item in member_roles if item is not None] await member.add_roles(*member_roles, atomic=False) logger.info("Created Top Topic Roles") async def on_member_join(self, member: Member): + # TODO use relationship and join topics: List[BTPUser] = await db.all(select(BTPUser).filter_by(user_id=member.id)) role_ids: List[int] = [(await db.first(select(BTPTopic).filter_by(id=topic))).role_id for topic in topics] roles: List[Role] = [self.bot.guilds[0].get_role(role_id) for role_id in role_ids] diff --git a/general/betheprofessional/models.py b/general/betheprofessional/models.py index 2f5610914..d21cc7c3d 100644 --- a/general/betheprofessional/models.py +++ b/general/betheprofessional/models.py @@ -1,4 +1,6 @@ -from typing import Union, Optional +from __future__ import annotations + +from typing import Union, Optional # TODO remove typing lib from PyDrocsid.database import db, Base from sqlalchemy import Column, BigInteger, Boolean, Integer, String, ForeignKey @@ -7,15 +9,15 @@ class BTPTopic(Base): __tablename__ = "btp_topic" id: Union[Column, int] = Column(Integer, primary_key=True) - name: Union[Column, str] = Column(String(255)) - parent: Union[Column, int] = Column(Integer) - role_id: Union[Column, int] = Column(BigInteger) + name: Union[Column, str] = Column(String(255)) # TODO unique!? + parent: Union[Column, int] = Column(Integer) # TODO foreign key? + role_id: Union[Column, int] = Column(BigInteger) # TODO unique!? assignable: Union[Column, bool] = Column(Boolean) @staticmethod async def create( - name: str, role_id: Union[int, None], assignable: bool, parent: Optional[Union[int, None]] - ) -> "BTPTopic": + name: str, role_id: Union[int, None], assignable: bool, parent: Optional[Union[int, None]] # TODO Optional Union?? + ) -> "BTPTopic": # TODO no quotes please row = BTPTopic(name=name, role_id=role_id, parent=parent, assignable=assignable) await db.add(row) return row @@ -26,10 +28,10 @@ class BTPUser(Base): id: Union[Column, int] = Column(Integer, primary_key=True) user_id: Union[Column, int] = Column(BigInteger) - topic: Union[Column, int] = Column(Integer, ForeignKey(BTPTopic.id)) + topic: Union[Column, int] = Column(Integer, ForeignKey(BTPTopic.id)) # TODO use relationship @staticmethod - async def create(user_id: int, topic: int) -> "BTPUser": + async def create(user_id: int, topic: int) -> BTPUser: # TODO no quotes please row = BTPUser(user_id=user_id, topic=topic) await db.add(row) return row diff --git a/general/betheprofessional/settings.py b/general/betheprofessional/settings.py index 8903bd26b..3222c6894 100644 --- a/general/betheprofessional/settings.py +++ b/general/betheprofessional/settings.py @@ -2,6 +2,7 @@ class BeTheProfessionalSettings(Settings): + # TODO add comments to explain the settings RoleLimit = 100 RoleCreateMinUsers = 1 # TODO diff --git a/general/betheprofessional/translations/en.yml b/general/betheprofessional/translations/en.yml index 446b79d23..b624d51cb 100644 --- a/general/betheprofessional/translations/en.yml +++ b/general/betheprofessional/translations/en.yml @@ -8,7 +8,7 @@ missing_cache_bypass_permission: "Missing Cache bypass Permission" betheprofessional: BeTheProfessional youre_not_the_first_one: "Topic `{}` not found.\nYou're not the first one to try this, {}" topic_not_found: Topic `{}` not found. -topic_not_found_did_you_mean: Topic `{}` not found. Did you mean `{}`? +topic_not_found_did_you_mean: Topic `{}` not found. Did you mean `{}`? # TODO use mentions available_topics_header: "Available Topics" no_topics_registered: No topics have been registered yet. @@ -26,8 +26,8 @@ topic_invalid_chars: Topic name `{}` contains invalid characters. topic_too_long: Topic name `{}` is too long. topic_already_registered: Topic `{}` has already been registered. topic_not_registered: Topic `{}` has not been registered. -topic_not_registered_too_high: Topic could not be registered because `@{}` is higher than `@{}`. -topic_not_registered_managed_role: Topic could not be registered because `@{}` cannot be assigned manually. +topic_not_registered_too_high: Topic could not be registered because `@{}` is higher than `@{}`. # TODO use mentions +topic_not_registered_managed_role: Topic could not be registered because `@{}` cannot be assigned manually. # TODO use mentions topics_registered: one: "Topic has been registered successfully. :white_check_mark:" @@ -47,8 +47,8 @@ single_un_assign_help: "Hey, did you know, that you can assign multiple topics b user_topics: zero: "{} has no topics assigned" - one: "{} has assigned the following topic: {}" - many: "{} has assigned the following topics: {}" + one: "{} has assigned the following topic: {}" # TODO use mentions + many: "{} has assigned the following topics: {}" # TODO use mentions parent_not_exists: "Parent `{}` doesn't exists" parent_format_help: "Please write `[Parents/]Topic-Name`" @@ -66,8 +66,8 @@ leaderboard_colmn_name: "[NAME]" leaderboard_colmn_users: "[USERS]" leaderboard_title: "Top `{}` - Most assigned Topics" -must_be_above_zero: "{} must be above zero!" -must_be_zero_or_above: "{} must be zero or above!" +must_be_above_zero: "{} must be above zero!" # TODO use quotes +must_be_zero_or_above: "{} must be zero or above!" # TODO use quotes settings: role_limit: @@ -85,4 +85,4 @@ settings: leaderboard_max_n: name: "Leaderboard Max N" internal_name: "LeaderboardMaxN" - updated: "Leaderboard Max N is now `{}`" \ No newline at end of file + updated: "Leaderboard Max N is now `{}`" From b5d33c3d87a41dcbdde5f59e825a5f8b090bfd23 Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Wed, 13 Apr 2022 16:11:05 +0200 Subject: [PATCH 56/68] Removed typing import --- general/betheprofessional/cog.py | 83 ++++++++++++++++---------------- 1 file changed, 41 insertions(+), 42 deletions(-) diff --git a/general/betheprofessional/cog.py b/general/betheprofessional/cog.py index de5d44a32..6455a9f6f 100644 --- a/general/betheprofessional/cog.py +++ b/general/betheprofessional/cog.py @@ -1,5 +1,4 @@ import string -from typing import List, Union, Optional, Dict, Final, Set, Tuple # TODO remove typing lib from discord import Member, Embed, Role, Message from discord.ext import commands, tasks @@ -27,19 +26,19 @@ logger = get_logger(__name__) -LEADERBOARD_TABLE_SPACING: Final = 2 +LEADERBOARD_TABLE_SPACING = 2 -def split_topics(topics: str) -> List[str]: +def split_topics(topics: str) -> list[str]: return [topic for topic in map(str.strip, topics.replace(";", ",").split(",")) if topic] -async def split_parents(topics: List[str], assignable: bool) -> List[tuple[str, bool, Optional[list[BTPTopic]]]]: - result: List[tuple[str, bool, Optional[list[BTPTopic]]]] = [] +async def split_parents(topics: list[str], assignable: bool) -> list[tuple[str, bool, list[BTPTopic]] | None]: + result: list[tuple[str, bool, list[BTPTopic]] | None] = [] for topic in topics: topic_tree = topic.split("/") - parents: List[Union[BTPTopic, None, CommandError]] = [ + parents: list[BTPTopic | None | CommandError] = [ # TODO use filter_by provided by the library await db.first(select(BTPTopic).filter_by(name=topic)) # TODO use filter_by provided by the library @@ -58,9 +57,9 @@ async def split_parents(topics: List[str], assignable: bool) -> List[tuple[str, return result -async def parse_topics(topics_str: str) -> List[BTPTopic]: - topics: List[BTPTopic] = [] - all_topics: List[BTPTopic] = await get_topics() +async def parse_topics(topics_str: str) -> list[BTPTopic]: + topics: list[BTPTopic] = [] + all_topics: list[BTPTopic] = await get_topics() if len(all_topics) == 0: raise CommandError(t.no_topics_registered) @@ -86,8 +85,8 @@ def dist(name: str) -> int: return topics -async def get_topics() -> List[BTPTopic]: - topics: List[BTPTopic] = [] +async def get_topics() -> list[BTPTopic]: + topics: list[BTPTopic] = [] async for topic in await db.stream(select(BTPTopic)): topics.append(topic) return topics @@ -111,11 +110,11 @@ async def on_ready(self): @commands.command(name="?") @guild_only() - async def list_topics(self, ctx: Context, parent_topic: Optional[str]): + async def list_topics(self, ctx: Context, parent_topic: str | None): """ list all direct children topics of the parent """ - parent: Union[BTPTopic, None, CommandError] = ( + parent: BTPTopic | None | CommandError = ( None if parent_topic is None # TODO use filter_by provided by the library @@ -126,9 +125,9 @@ async def list_topics(self, ctx: Context, parent_topic: Optional[str]): raise parent embed = Embed(title=t.available_topics_header, colour=Colors.BeTheProfessional) - sorted_topics: Dict[str, List[str]] = {} + sorted_topics: dict[str, list[str]] = {} # TODO use filter_by provided by the library - topics: List[BTPTopic] = await db.all(select(BTPTopic).filter_by(parent=None if parent is None else parent.id)) + topics: list[BTPTopic] = await db.all(select(BTPTopic).filter_by(parent=None if parent is None else parent.id)) if not topics: embed.colour = Colors.error embed.description = t.no_topics_registered @@ -136,7 +135,7 @@ async def list_topics(self, ctx: Context, parent_topic: Optional[str]): return topics.sort(key=lambda btp_topic: btp_topic.name.lower()) - root_topic: Union[BTPTopic, None] = ( + root_topic: BTPTopic | None = ( # TODO use filter_by provided by the library None if parent_topic is None else await db.first(select(BTPTopic).filter_by(name=parent_topic)) ) @@ -173,7 +172,7 @@ async def assign_topics(self, ctx: Context, *, topics: str): """ member: Member = ctx.author - topics: List[BTPTopic] = [ + topics: list[BTPTopic] = [ topic for topic in await parse_topics(topics) # TODO use filter_by provided by the library @@ -182,7 +181,7 @@ async def assign_topics(self, ctx: Context, *, topics: str): and not (await db.exists(select(BTPUser).filter_by(user_id=member.id, topic=topic.id))) # noqa: W503 ] - roles: List[Role] = [] + roles: list[Role] = [] for topic in topics: await BTPUser.create(member.id, topic.id) @@ -219,16 +218,16 @@ async def unassign_topics(self, ctx: Context, *, topics: str): """ member: Member = ctx.author if topics.strip() == "*": - topics: List[BTPTopic] = await get_topics() + topics: list[BTPTopic] = await get_topics() else: - topics: List[BTPTopic] = await parse_topics(topics) - affected_topics: List[BTPTopic] = [] + topics: list[BTPTopic] = await parse_topics(topics) + affected_topics: list[BTPTopic] = [] for topic in topics: # TODO use filter_by provided by the library if await db.exists(select(BTPUser).filter_by(user_id=member.id, topic=topic.id)): affected_topics.append(topic) - roles: List[Role] = [] + roles: list[Role] = [] for topic in affected_topics: # TODO use filter_by provided by the library @@ -266,12 +265,12 @@ async def register_topics(self, ctx: Context, *, topic_paths: str, assignable: b """ names = split_topics(topic_paths) - topic_paths: List[tuple[str, bool, Optional[list[BTPTopic]]]] = await split_parents(names, assignable) + topic_paths: list[tuple[str, bool, list[BTPTopic] | None]] = await split_parents(names, assignable) if not names or not topic_paths: raise UserInputError valid_chars = set(string.ascii_letters + string.digits + " !#$%&'()+-./:<=>?[\\]^_`{|}~") - registered_topics: List[tuple[str, bool, Optional[list[BTPTopic]]]] = [] + registered_topics: list[tuple[str, bool, list[BTPTopic]] | None] = [] for topic in topic_paths: if len(topic) > 100: raise CommandError(t.topic_too_long(topic)) @@ -307,7 +306,7 @@ async def delete_topics(self, ctx: Context, *, topics: str): delete one or more topics """ - topics: List[str] = split_topics(topics) + topics: list[str] = split_topics(topics) delete_topics: list[BTPTopic] = [] @@ -352,7 +351,7 @@ async def delete_topics(self, ctx: Context, *, topics: str): @commands.command() @guild_only() - async def topic(self, ctx: Context, topic_name: str, message: Optional[Message]): + async def topic(self, ctx: Context, topic_name: str, message: Message | None): """ pings the specified topic """ @@ -364,9 +363,9 @@ async def topic(self, ctx: Context, topic_name: str, message: Optional[Message]) if topic.role_id is not None: mention = ctx.guild.get_role(topic.role_id).mention # TODO use <@&ID> else: - topic_members: List[BTPUser] = await db.all(select(BTPUser).filter_by(topic=topic.id)) + topic_members: list[BTPUser] = await db.all(select(BTPUser).filter_by(topic=topic.id)) # TODO what if member does not exist? Why don't you use `<@ID>`? - members: List[Member] = [ctx.guild.get_member(member.user_id) for member in topic_members] + members: list[Member] = [ctx.guild.get_member(member.user_id) for member in topic_members] mention = ", ".join(map(lambda m: m.mention, members)) if mention == "": @@ -461,7 +460,7 @@ async def leaderboard_max_n(self, ctx: Context, leaderboard_max_n: int): @btp.command(aliases=["lb"]) @guild_only() # TODO parameters - async def leaderboard(self, ctx: Context, n: Optional[int] = None, use_cache: bool = True): + async def leaderboard(self, ctx: Context, n: int | None = None, use_cache: bool = True): """ lists the top n topics """ @@ -478,7 +477,7 @@ async def leaderboard(self, ctx: Context, n: Optional[int] = None, use_cache: bo if n <= 0: raise CommandError(t.leaderboard_n_zero_error) - cached_leaderboard_parts: Optional[list[str]] = None + cached_leaderboard_parts: list[str] | None = None redis_key = f"btp:leaderboard:n:{n}" if use_cache: @@ -488,12 +487,12 @@ async def leaderboard(self, ctx: Context, n: Optional[int] = None, use_cache: bo leaderboard_parts: list[str] = [] if not cached_leaderboard_parts: - topic_count: Dict[int, int] = {} + topic_count: dict[int, int] = {} for topic in await db.all(select(BTPTopic)): topic_count[topic.id] = await db.count(select(BTPUser).filter_by(topic=topic.id)) - top_topics: List[int] = sorted(topic_count, key=lambda x: topic_count[x], reverse=True)[:n] + top_topics: list[int] = sorted(topic_count, key=lambda x: topic_count[x], reverse=True)[:n] if len(top_topics) == 0: raise CommandError(t.no_topics_registered) @@ -539,7 +538,7 @@ async def leaderboard(self, ctx: Context, n: Optional[int] = None, use_cache: bo await send_long_embed(ctx, embed, paginate=True) @commands.command(name="usertopics", aliases=["usertopic", "utopics", "utopic"]) - async def user_topics(self, ctx: Context, member: Optional[Member]): + async def user_topics(self, ctx: Context, member: Member | None): """ lists all topics of a member """ @@ -585,7 +584,7 @@ async def update_roles(self): role_create_min_users = await BeTheProfessionalSettings.RoleCreateMinUsers.get() logger.info("Started Update Role Loop") - topic_count: Dict[int, int] = {} + topic_count: dict[int, int] = {} # TODO rewrite from here.... for topic in await db.all(select(BTPTopic)): @@ -594,12 +593,12 @@ async def update_roles(self): # not using dict.items() because of typing # TODO Let db sort topics by count and then by # TODO fix TODO ^^ - topic_count_items: list[Tuple[int, int]] = list(zip(topic_count.keys(), topic_count.values())) + topic_count_items: list[tuple[int, int]] = list(zip(topic_count.keys(), topic_count.values())) topic_count = dict(sorted(topic_count_items, key=lambda x: x[0])) # Sort Topics By Count, Keep only Topics with a Count of BeTheProfessionalSettings.RoleCreateMinUsers or above # Limit Roles to BeTheProfessionalSettings.RoleLimit - top_topics: List[int] = list( + top_topics: list[int] = list( filter( lambda topic_id: topic_count[topic_id] >= role_create_min_users, sorted(topic_count, key=lambda x: topic_count[x], reverse=True), @@ -618,7 +617,7 @@ async def update_roles(self): topic.role_id = None # Create new Topic Roles - roles: Dict[int, Role] = {} + roles: dict[int, Role] = {} # TODO use sql "IN" expression for top_topic in top_topics: topic: BTPTopic = await db.first(select(BTPTopic).filter_by(id=top_topic)) @@ -629,12 +628,12 @@ async def update_roles(self): # Iterate over all members(with topics) and add the role to them # TODO add filter, only select topics with newly added roles - member_ids: Set[int] = {btp_user.user_id for btp_user in await db.all(select(BTPUser))} + member_ids: set[int] = {btp_user.user_id for btp_user in await db.all(select(BTPUser))} for member_id in member_ids: member: Member = self.bot.guilds[0].get_member(member_id) if member is None: continue - member_roles: List[Role] = [ + member_roles: list[Role] = [ roles.get(btp_user.topic) for btp_user in await db.all(select(BTPUser).filter_by(user_id=member_id)) ] # TODO use filter or something? @@ -645,7 +644,7 @@ async def update_roles(self): async def on_member_join(self, member: Member): # TODO use relationship and join - topics: List[BTPUser] = await db.all(select(BTPUser).filter_by(user_id=member.id)) - role_ids: List[int] = [(await db.first(select(BTPTopic).filter_by(id=topic))).role_id for topic in topics] - roles: List[Role] = [self.bot.guilds[0].get_role(role_id) for role_id in role_ids] + topics: list[BTPUser] = await db.all(select(BTPUser).filter_by(user_id=member.id)) + role_ids: list[int] = [(await db.first(select(BTPTopic).filter_by(id=topic))).role_id for topic in topics] + roles: list[Role] = [self.bot.guilds[0].get_role(role_id) for role_id in role_ids] await member.add_roles(*roles, atomic=False) From c4465843f754be5456ba4fae12466166e08cae40 Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Wed, 13 Apr 2022 16:58:07 +0200 Subject: [PATCH 57/68] Resolved some TODO's --- general/betheprofessional/cog.py | 58 +++++++++++------------------ general/betheprofessional/models.py | 22 +++++------ 2 files changed, 31 insertions(+), 49 deletions(-) diff --git a/general/betheprofessional/cog.py b/general/betheprofessional/cog.py index 6455a9f6f..a8c11012a 100644 --- a/general/betheprofessional/cog.py +++ b/general/betheprofessional/cog.py @@ -7,7 +7,7 @@ import PyDrocsid.embeds from PyDrocsid.cog import Cog from PyDrocsid.command import reply -from PyDrocsid.database import db, select, db_wrapper +from PyDrocsid.database import db, select, db_wrapper, filter_by from PyDrocsid.embeds import send_long_embed from PyDrocsid.environment import CACHE_TTL from PyDrocsid.logger import get_logger @@ -39,10 +39,9 @@ async def split_parents(topics: list[str], assignable: bool) -> list[tuple[str, topic_tree = topic.split("/") parents: list[BTPTopic | None | CommandError] = [ - # TODO use filter_by provided by the library - await db.first(select(BTPTopic).filter_by(name=topic)) - # TODO use filter_by provided by the library - if await db.exists(select(BTPTopic).filter_by(name=topic)) + await db.first(filter_by(BTPTopic, name=topic)) + + if await db.exists(filter_by(BTPTopic, name=topic)) else CommandError(t.parent_not_exists(topic)) for topic in topic_tree[:-1] ] @@ -65,8 +64,7 @@ async def parse_topics(topics_str: str) -> list[BTPTopic]: raise CommandError(t.no_topics_registered) for topic_name in split_topics(topics_str): - # TODO use filter_by provided by the library - topic = await db.first(select(BTPTopic).filter_by(name=topic_name)) + topic = await db.first(filter_by(BTPTopic, name=topic_name)) if topic is None and len(all_topics) > 0: @@ -117,8 +115,7 @@ async def list_topics(self, ctx: Context, parent_topic: str | None): parent: BTPTopic | None | CommandError = ( None if parent_topic is None - # TODO use filter_by provided by the library - else await db.first(select(BTPTopic).filter_by(name=parent_topic)) + else await db.first(filter_by(BTPTopic, name=parent_topic)) or CommandError(t.topic_not_found(parent_topic)) # noqa: W503 ) if isinstance(parent, CommandError): @@ -126,8 +123,7 @@ async def list_topics(self, ctx: Context, parent_topic: str | None): embed = Embed(title=t.available_topics_header, colour=Colors.BeTheProfessional) sorted_topics: dict[str, list[str]] = {} - # TODO use filter_by provided by the library - topics: list[BTPTopic] = await db.all(select(BTPTopic).filter_by(parent=None if parent is None else parent.id)) + topics: list[BTPTopic] = await db.all(filter_by(BTPTopic, parent=None if parent is None else parent.id)) if not topics: embed.colour = Colors.error embed.description = t.no_topics_registered @@ -136,8 +132,7 @@ async def list_topics(self, ctx: Context, parent_topic: str | None): topics.sort(key=lambda btp_topic: btp_topic.name.lower()) root_topic: BTPTopic | None = ( - # TODO use filter_by provided by the library - None if parent_topic is None else await db.first(select(BTPTopic).filter_by(name=parent_topic)) + None if parent_topic is None else await db.first(filter_by(BTPTopic, name=parent_topic)) ) for topic in topics: if (root_topic.name if root_topic is not None else "Topics") not in sorted_topics.keys(): @@ -153,8 +148,7 @@ async def list_topics(self, ctx: Context, parent_topic: str | None): f"`{topic.name}" + ( # noqa: W503 f" ({c})`" - # TODO use filter_by provided by the library - if (c := await db.count(select(BTPTopic).filter_by(parent=topic.id))) > 0 + if (c := await db.count(filter_by(BTPTopic, parent=topic.id))) > 0 else "`" ) for topic in topics @@ -175,10 +169,8 @@ async def assign_topics(self, ctx: Context, *, topics: str): topics: list[BTPTopic] = [ topic for topic in await parse_topics(topics) - # TODO use filter_by provided by the library - if (await db.exists(select(BTPTopic).filter_by(id=topic.id))) - # TODO use filter_by provided by the library - and not (await db.exists(select(BTPUser).filter_by(user_id=member.id, topic=topic.id))) # noqa: W503 + if (await db.exists(filter_by(BTPTopic, id=topic.id))) + and not (await db.exists(filter_by(BTPUser, user_id=member.id, topic=topic.id))) # noqa: W503 ] roles: list[Role] = [] @@ -223,15 +215,13 @@ async def unassign_topics(self, ctx: Context, *, topics: str): topics: list[BTPTopic] = await parse_topics(topics) affected_topics: list[BTPTopic] = [] for topic in topics: - # TODO use filter_by provided by the library - if await db.exists(select(BTPUser).filter_by(user_id=member.id, topic=topic.id)): + if await db.exists(filter_by(BTPUser, user_id=member.id, topic=topic.id)): affected_topics.append(topic) roles: list[Role] = [] for topic in affected_topics: - # TODO use filter_by provided by the library - await db.delete(await db.first(select(BTPUser).filter_by(topic=topic.id))) + await db.delete(await db.first(filter_by(BTPUser, topic=topic.id))) if topic.role_id: roles.append(ctx.guild.get_role(topic.role_id)) @@ -277,8 +267,7 @@ async def register_topics(self, ctx: Context, *, topic_paths: str, assignable: b if any(c not in valid_chars for c in topic[0]): raise CommandError(t.topic_invalid_chars(topic)) - # TODO use filter_by provided by the library - if await db.exists(select(BTPTopic).filter_by(name=topic[0])): + if await db.exists(filter_by(BTPTopic, name=topic[0])): raise CommandError(t.topic_already_registered(topic[0])) else: registered_topics.append(topic) @@ -312,11 +301,9 @@ async def delete_topics(self, ctx: Context, *, topics: str): for topic in topics: # TODO two selects for the same thing? and use filter_by provided by the library - if not await db.exists(select(BTPTopic).filter_by(name=topic)): + if not (btp_topic := await db.exists(filter_by(BTPTopic, name=topic))): raise CommandError(t.topic_not_registered(topic)) else: - btp_topic: BTPTopic = await db.first(select(BTPTopic).filter_by(name=topic)) - delete_topics.append(btp_topic) # TODO use relationships for children @@ -330,11 +317,10 @@ async def delete_topics(self, ctx: Context, *, topics: str): for topic in delete_topics: if topic.role_id is not None: - # TODO what if role is None? role: Role = ctx.guild.get_role(topic.role_id) - await role.delete() - # TODO use filter_by provided by the library - for user_topic in await db.all(select(BTPUser).filter_by(topic=topic.id)): + if role is not None: + await role.delete() + for user_topic in await db.all(filter_by(BTPUser, topic=topic.id)): # TODO use db.exec await db.delete(user_topic) # TODO do not commit for each one separately @@ -361,12 +347,10 @@ async def topic(self, ctx: Context, topic_name: str, message: Message | None): if topic is None: raise CommandError(t.topic_not_found(topic_name)) if topic.role_id is not None: - mention = ctx.guild.get_role(topic.role_id).mention # TODO use <@&ID> + mention = f"<@&{topic.role_id}>" else: topic_members: list[BTPUser] = await db.all(select(BTPUser).filter_by(topic=topic.id)) - # TODO what if member does not exist? Why don't you use `<@ID>`? - members: list[Member] = [ctx.guild.get_member(member.user_id) for member in topic_members] - mention = ", ".join(map(lambda m: m.mention, members)) + mention = ", ".join(map(lambda m: f"<@{m.user_id}>", topic_members)) if mention == "": raise CommandError(t.nobody_has_topic(topic_name)) @@ -609,7 +593,7 @@ async def update_roles(self): # Delete old Top Topic Roles # TODO use filter_by - for topic in await db.all(select(BTPTopic).filter(BTPTopic.role_id is not None)): # type: BTPTopic + for topic in await db.all(select().filter(BTPTopic.role_id is not None)): # type: BTPTopic # TODO use sql "NOT IN" expression if topic.id not in top_topics: if topic.role_id is not None: diff --git a/general/betheprofessional/models.py b/general/betheprofessional/models.py index d21cc7c3d..756f7fef5 100644 --- a/general/betheprofessional/models.py +++ b/general/betheprofessional/models.py @@ -1,6 +1,5 @@ from __future__ import annotations -from typing import Union, Optional # TODO remove typing lib from PyDrocsid.database import db, Base from sqlalchemy import Column, BigInteger, Boolean, Integer, String, ForeignKey @@ -8,16 +7,15 @@ class BTPTopic(Base): __tablename__ = "btp_topic" - id: Union[Column, int] = Column(Integer, primary_key=True) - name: Union[Column, str] = Column(String(255)) # TODO unique!? - parent: Union[Column, int] = Column(Integer) # TODO foreign key? - role_id: Union[Column, int] = Column(BigInteger) # TODO unique!? - assignable: Union[Column, bool] = Column(Boolean) + id: Column | int = Column(Integer, primary_key=True) + name: Column | str = Column(String(255), unique=True) + parent: Column | int = Column(Integer) # TODO foreign key? + role_id: Column | int = Column(BigInteger, unique=True) + assignable: Column | bool = Column(Boolean) @staticmethod async def create( - name: str, role_id: Union[int, None], assignable: bool, parent: Optional[Union[int, None]] # TODO Optional Union?? - ) -> "BTPTopic": # TODO no quotes please + name: str, role_id: int | None, assignable: bool, parent: int | None) -> BTPTopic: row = BTPTopic(name=name, role_id=role_id, parent=parent, assignable=assignable) await db.add(row) return row @@ -26,12 +24,12 @@ async def create( class BTPUser(Base): __tablename__ = "btp_users" - id: Union[Column, int] = Column(Integer, primary_key=True) - user_id: Union[Column, int] = Column(BigInteger) - topic: Union[Column, int] = Column(Integer, ForeignKey(BTPTopic.id)) # TODO use relationship + id: Column | int = Column(Integer, primary_key=True) + user_id: Column | int = Column(BigInteger) + topic: Column | int = Column(Integer, ForeignKey(BTPTopic.id)) # TODO use relationship @staticmethod - async def create(user_id: int, topic: int) -> BTPUser: # TODO no quotes please + async def create(user_id: int, topic: int) -> BTPUser: row = BTPUser(user_id=user_id, topic=topic) await db.add(row) return row From 38ffb81ec17bfe8f2501b69b4efd33b5e1b2cd59 Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Wed, 13 Apr 2022 20:12:21 +0200 Subject: [PATCH 58/68] Resolved some TODO's --- general/betheprofessional/cog.py | 37 +++++++++---------- general/betheprofessional/translations/en.yml | 12 +++--- 2 files changed, 22 insertions(+), 27 deletions(-) diff --git a/general/betheprofessional/cog.py b/general/betheprofessional/cog.py index a8c11012a..66f2f1980 100644 --- a/general/betheprofessional/cog.py +++ b/general/betheprofessional/cog.py @@ -90,6 +90,17 @@ async def get_topics() -> list[BTPTopic]: return topics +async def change_setting(ctx: Context, name: str, value: any): + data = t.settings[name] + await getattr(BeTheProfessionalSettings, data["internal_name"]).set(value) + + embed = Embed(title=t.betheprofessional, color=Colors.green) + embed.description = data["updated"].format(value) + + await reply(ctx, embed=embed) + await send_to_changelog(ctx.guild, embed.description) + + class BeTheProfessionalCog(Cog, name="BeTheProfessional"): CONTRIBUTORS = [ Contributor.Defelo, @@ -300,7 +311,6 @@ async def delete_topics(self, ctx: Context, *, topics: str): delete_topics: list[BTPTopic] = [] for topic in topics: - # TODO two selects for the same thing? and use filter_by provided by the library if not (btp_topic := await db.exists(filter_by(BTPTopic, name=topic))): raise CommandError(t.topic_not_registered(topic)) else: @@ -323,8 +333,7 @@ async def delete_topics(self, ctx: Context, *, topics: str): for user_topic in await db.all(filter_by(BTPUser, topic=topic.id)): # TODO use db.exec await db.delete(user_topic) - # TODO do not commit for each one separately - await db.commit() + await db.commit() await db.delete(topic) embed = Embed(title=t.betheprofessional, colour=Colors.BeTheProfessional) @@ -377,18 +386,6 @@ async def btp(self, ctx: Context): ) await reply(ctx, embed=embed) - # TODO make function, not method, self not used - async def change_setting(self, ctx: Context, name: str, value: any): - # TODO use dictionary - data = getattr(t.settings, name) - await getattr(BeTheProfessionalSettings, data.internal_name).set(value) - - embed = Embed(title=t.betheprofessional, color=Colors.green) - embed.description = data.updated(value) - - await reply(ctx, embed=embed) - await send_to_changelog(ctx.guild, embed.description) - @btp.command() @guild_only() @BeTheProfessionalPermission.manage.check @@ -400,7 +397,7 @@ async def role_limit(self, ctx: Context, role_limit: int): if role_limit <= 0: # TODO use quotes for the name in the embed raise CommandError(t.must_be_above_zero(t.settings.role_limit.name)) - await self.change_setting(ctx, "role_limit", role_limit) + await change_setting(ctx, "role_limit", role_limit) @btp.command() @guild_only() @@ -413,7 +410,7 @@ async def role_create_min_users(self, ctx: Context, role_create_min_users: int): if role_create_min_users < 0: # TODO use quotes for the name in the embed raise CommandError(t.must_be_zero_or_above(t.settings.role_create_min_users.name)) - await self.change_setting(ctx, "role_create_min_users", role_create_min_users) + await change_setting(ctx, "role_create_min_users", role_create_min_users) @btp.command() @guild_only() @@ -426,7 +423,7 @@ async def leaderboard_default_n(self, ctx: Context, leaderboard_default_n: int): if leaderboard_default_n <= 0: # TODO use quotes for the name in the embed raise CommandError(t.must_be_above_zero(t.settings.leaderboard_default_n.name)) - await self.change_setting(ctx, "leaderboard_default_n", leaderboard_default_n) + await change_setting(ctx, "leaderboard_default_n", leaderboard_default_n) @btp.command() @guild_only() @@ -439,7 +436,7 @@ async def leaderboard_max_n(self, ctx: Context, leaderboard_max_n: int): if leaderboard_max_n <= 0: # TODO use quotes for the name in the embed raise CommandError(t.must_be_above_zero(t.settings.leaderboard_max_n.name)) - await self.change_setting(ctx, "leaderboard_max_n", leaderboard_max_n) + await change_setting(ctx, "leaderboard_max_n", leaderboard_max_n) @btp.command(aliases=["lb"]) @guild_only() @@ -593,7 +590,7 @@ async def update_roles(self): # Delete old Top Topic Roles # TODO use filter_by - for topic in await db.all(select().filter(BTPTopic.role_id is not None)): # type: BTPTopic + for topic in await db.all(select(BTPTopic).filter(BTPTopic.role_id is not None)): # type: BTPTopic # TODO use sql "NOT IN" expression if topic.id not in top_topics: if topic.role_id is not None: diff --git a/general/betheprofessional/translations/en.yml b/general/betheprofessional/translations/en.yml index b624d51cb..df24ed464 100644 --- a/general/betheprofessional/translations/en.yml +++ b/general/betheprofessional/translations/en.yml @@ -8,7 +8,7 @@ missing_cache_bypass_permission: "Missing Cache bypass Permission" betheprofessional: BeTheProfessional youre_not_the_first_one: "Topic `{}` not found.\nYou're not the first one to try this, {}" topic_not_found: Topic `{}` not found. -topic_not_found_did_you_mean: Topic `{}` not found. Did you mean `{}`? # TODO use mentions +topic_not_found_did_you_mean: Topic `{}` not found. Did you mean `{}`? available_topics_header: "Available Topics" no_topics_registered: No topics have been registered yet. @@ -26,8 +26,6 @@ topic_invalid_chars: Topic name `{}` contains invalid characters. topic_too_long: Topic name `{}` is too long. topic_already_registered: Topic `{}` has already been registered. topic_not_registered: Topic `{}` has not been registered. -topic_not_registered_too_high: Topic could not be registered because `@{}` is higher than `@{}`. # TODO use mentions -topic_not_registered_managed_role: Topic could not be registered because `@{}` cannot be assigned manually. # TODO use mentions topics_registered: one: "Topic has been registered successfully. :white_check_mark:" @@ -47,8 +45,8 @@ single_un_assign_help: "Hey, did you know, that you can assign multiple topics b user_topics: zero: "{} has no topics assigned" - one: "{} has assigned the following topic: {}" # TODO use mentions - many: "{} has assigned the following topics: {}" # TODO use mentions + one: "{} has assigned the following topic: {}" + many: "{} has assigned the following topics: {}" parent_not_exists: "Parent `{}` doesn't exists" parent_format_help: "Please write `[Parents/]Topic-Name`" @@ -66,8 +64,8 @@ leaderboard_colmn_name: "[NAME]" leaderboard_colmn_users: "[USERS]" leaderboard_title: "Top `{}` - Most assigned Topics" -must_be_above_zero: "{} must be above zero!" # TODO use quotes -must_be_zero_or_above: "{} must be zero or above!" # TODO use quotes +must_be_above_zero: "`{}` must be above zero!" +must_be_zero_or_above: "`{}` must be zero or above!" settings: role_limit: From aa6202f3c17f97c8c4943fd6914b097d47ee9287 Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Mon, 16 May 2022 21:24:15 +0200 Subject: [PATCH 59/68] Added DB Relationships --- general/betheprofessional/cog.py | 70 ++++++------------- general/betheprofessional/models.py | 21 ++++-- general/betheprofessional/translations/en.yml | 8 +-- 3 files changed, 42 insertions(+), 57 deletions(-) diff --git a/general/betheprofessional/cog.py b/general/betheprofessional/cog.py index 66f2f1980..b2408ae4c 100644 --- a/general/betheprofessional/cog.py +++ b/general/betheprofessional/cog.py @@ -6,8 +6,8 @@ import PyDrocsid.embeds from PyDrocsid.cog import Cog -from PyDrocsid.command import reply -from PyDrocsid.database import db, select, db_wrapper, filter_by +from PyDrocsid.command import reply, Confirmation +from PyDrocsid.database import db, select, db_wrapper, filter_by, delete from PyDrocsid.embeds import send_long_embed from PyDrocsid.environment import CACHE_TTL from PyDrocsid.logger import get_logger @@ -134,7 +134,7 @@ async def list_topics(self, ctx: Context, parent_topic: str | None): embed = Embed(title=t.available_topics_header, colour=Colors.BeTheProfessional) sorted_topics: dict[str, list[str]] = {} - topics: list[BTPTopic] = await db.all(filter_by(BTPTopic, parent=None if parent is None else parent.id)) + topics: list[BTPTopic] = await db.all(filter_by(BTPTopic, parent_id=None if parent is None else parent.id)) if not topics: embed.colour = Colors.error embed.description = t.no_topics_registered @@ -159,7 +159,7 @@ async def list_topics(self, ctx: Context, parent_topic: str | None): f"`{topic.name}" + ( # noqa: W503 f" ({c})`" - if (c := await db.count(filter_by(BTPTopic, parent=topic.id))) > 0 + if (c := await db.count(filter_by(BTPTopic, parent_id=topic.id))) > 0 else "`" ) for topic in topics @@ -181,7 +181,7 @@ async def assign_topics(self, ctx: Context, *, topics: str): topic for topic in await parse_topics(topics) if (await db.exists(filter_by(BTPTopic, id=topic.id))) - and not (await db.exists(filter_by(BTPUser, user_id=member.id, topic=topic.id))) # noqa: W503 + and not (await db.exists(filter_by(BTPUser, user_id=member.id, topic_id=topic.id))) # noqa: W503 ] roles: list[Role] = [] @@ -226,13 +226,13 @@ async def unassign_topics(self, ctx: Context, *, topics: str): topics: list[BTPTopic] = await parse_topics(topics) affected_topics: list[BTPTopic] = [] for topic in topics: - if await db.exists(filter_by(BTPUser, user_id=member.id, topic=topic.id)): + if await db.exists(filter_by(BTPUser, user_id=member.id, topic_id=topic.id)): affected_topics.append(topic) roles: list[Role] = [] for topic in affected_topics: - await db.delete(await db.first(filter_by(BTPUser, topic=topic.id))) + await db.delete(await db.first(filter_by(BTPUser, topic_id=topic.id))) if topic.role_id: roles.append(ctx.guild.get_role(topic.role_id)) @@ -284,6 +284,7 @@ async def register_topics(self, ctx: Context, *, topic_paths: str, assignable: b registered_topics.append(topic) for registered_topic in registered_topics: + # TODO: assignable? await BTPTopic.create( registered_topic[0], None, True, registered_topic[2][-1].id if len(registered_topic[2]) > 0 else None ) @@ -308,39 +309,21 @@ async def delete_topics(self, ctx: Context, *, topics: str): topics: list[str] = split_topics(topics) - delete_topics: list[BTPTopic] = [] + # TODO: confirm message to delete (multiple) topic(s) for topic in topics: - if not (btp_topic := await db.exists(filter_by(BTPTopic, name=topic))): + if not await db.exists(filter_by(BTPTopic, name=topic)): raise CommandError(t.topic_not_registered(topic)) - else: - delete_topics.append(btp_topic) - - # TODO use relationships for children - queue: list[int] = [btp_topic.id] - - while len(queue) != 0: - topic_id = queue.pop() - for child_topic in await db.all(select(BTPTopic).filter_by(parent=topic_id)): - delete_topics.insert(0, child_topic) - queue.append(child_topic.id) - - for topic in delete_topics: - if topic.role_id is not None: - role: Role = ctx.guild.get_role(topic.role_id) - if role is not None: - await role.delete() - for user_topic in await db.all(filter_by(BTPUser, topic=topic.id)): - # TODO use db.exec - await db.delete(user_topic) - await db.commit() - await db.delete(topic) + + #if not await Confirm(ctx.author, True, ) + for topic in topics: + await db.exec(delete(BTPTopic).where(BTPTopic.name == topic)) embed = Embed(title=t.betheprofessional, colour=Colors.BeTheProfessional) - embed.description = t.topics_unregistered(cnt=len(delete_topics)) + embed.description = t.topics_unregistered(cnt=len(topics)) await send_to_changelog( ctx.guild, - t.log_topics_unregistered(cnt=len(delete_topics), topics=", ".join(f"`{t.name}`" for t in delete_topics)), + t.log_topics_unregistered(cnt=len(topics), topics=", ".join(f"`{t}`" for t in topics)), ) await send_long_embed(ctx, embed) @@ -358,7 +341,7 @@ async def topic(self, ctx: Context, topic_name: str, message: Message | None): if topic.role_id is not None: mention = f"<@&{topic.role_id}>" else: - topic_members: list[BTPUser] = await db.all(select(BTPUser).filter_by(topic=topic.id)) + topic_members: list[BTPUser] = await db.all(select(BTPUser).filter_by(topic_id=topic.id)) mention = ", ".join(map(lambda m: f"<@{m.user_id}>", topic_members)) if mention == "": @@ -395,7 +378,6 @@ async def role_limit(self, ctx: Context, role_limit: int): """ if role_limit <= 0: - # TODO use quotes for the name in the embed raise CommandError(t.must_be_above_zero(t.settings.role_limit.name)) await change_setting(ctx, "role_limit", role_limit) @@ -408,7 +390,6 @@ async def role_create_min_users(self, ctx: Context, role_create_min_users: int): """ if role_create_min_users < 0: - # TODO use quotes for the name in the embed raise CommandError(t.must_be_zero_or_above(t.settings.role_create_min_users.name)) await change_setting(ctx, "role_create_min_users", role_create_min_users) @@ -421,7 +402,6 @@ async def leaderboard_default_n(self, ctx: Context, leaderboard_default_n: int): """ if leaderboard_default_n <= 0: - # TODO use quotes for the name in the embed raise CommandError(t.must_be_above_zero(t.settings.leaderboard_default_n.name)) await change_setting(ctx, "leaderboard_default_n", leaderboard_default_n) @@ -434,7 +414,6 @@ async def leaderboard_max_n(self, ctx: Context, leaderboard_max_n: int): """ if leaderboard_max_n <= 0: - # TODO use quotes for the name in the embed raise CommandError(t.must_be_above_zero(t.settings.leaderboard_max_n.name)) await change_setting(ctx, "leaderboard_max_n", leaderboard_max_n) @@ -471,7 +450,7 @@ async def leaderboard(self, ctx: Context, n: int | None = None, use_cache: bool topic_count: dict[int, int] = {} for topic in await db.all(select(BTPTopic)): - topic_count[topic.id] = await db.count(select(BTPUser).filter_by(topic=topic.id)) + topic_count[topic.id] = await db.count(select(BTPUser).filter_by(topic_id=topic.id)) top_topics: list[int] = sorted(topic_count, key=lambda x: topic_count[x], reverse=True)[:n] @@ -528,10 +507,7 @@ async def user_topics(self, ctx: Context, member: Member | None): member = ctx.author # TODO use relationships and join - topics_assigns: list[BTPUser] = await db.all(select(BTPUser).filter_by(user_id=member.id)) - topics: list[BTPTopic] = [ - await db.first(select(BTPTopic).filter_by(id=assignment.topic)) for assignment in topics_assigns - ] + topics_assignments: list[BTPUser] = await db.all(select(BTPUser).filter_by(user_id=member.id)) embed = Embed(title=t.betheprofessional, color=Colors.BeTheProfessional) @@ -539,12 +515,12 @@ async def user_topics(self, ctx: Context, member: Member | None): topics_str: str = "" - if len(topics_assigns) == 0: + if len(topics_assignments) == 0: embed.colour = Colors.red else: - topics_str = ", ".join([f"`{topic.name}`" for topic in topics]) + topics_str = ", ".join([f"`{topics_assignment.topic.name}`" for topics_assignment in topics_assignments]) - embed.description = t.user_topics(member.mention, topics_str, cnt=len(topics)) + embed.description = t.user_topics(member.mention, topics_str, cnt=len(topics_assignments)) await reply(ctx, embed=embed) @@ -570,7 +546,7 @@ async def update_roles(self): # TODO rewrite from here.... for topic in await db.all(select(BTPTopic)): # TODO use relationship and join - topic_count[topic.id] = await db.count(select(BTPUser).filter_by(topic=topic.id)) + topic_count[topic.id] = await db.count(select(BTPUser).filter_by(topic_id=topic.id)) # not using dict.items() because of typing # TODO Let db sort topics by count and then by # TODO fix TODO ^^ diff --git a/general/betheprofessional/models.py b/general/betheprofessional/models.py index 756f7fef5..799c97649 100644 --- a/general/betheprofessional/models.py +++ b/general/betheprofessional/models.py @@ -1,5 +1,7 @@ from __future__ import annotations +from sqlalchemy.orm import relationship, backref + from PyDrocsid.database import db, Base from sqlalchemy import Column, BigInteger, Boolean, Integer, String, ForeignKey @@ -9,14 +11,20 @@ class BTPTopic(Base): id: Column | int = Column(Integer, primary_key=True) name: Column | str = Column(String(255), unique=True) - parent: Column | int = Column(Integer) # TODO foreign key? + parent_id: Column | int = Column(Integer, ForeignKey('btp_topic.id', ondelete='CASCADE')) + children: list[BTPTopic] = relationship( + "BTPTopic", + backref=backref("parent", remote_side=id, foreign_keys=[parent_id]), + lazy="subquery", + ) role_id: Column | int = Column(BigInteger, unique=True) + users: list[BTPUser] = relationship("BTPUser", back_populates="topic") assignable: Column | bool = Column(Boolean) @staticmethod async def create( - name: str, role_id: int | None, assignable: bool, parent: int | None) -> BTPTopic: - row = BTPTopic(name=name, role_id=role_id, parent=parent, assignable=assignable) + name: str, role_id: int | None, assignable: bool, parent_id: int | None) -> BTPTopic: + row = BTPTopic(name=name, role_id=role_id, parent_id=parent_id, assignable=assignable) await db.add(row) return row @@ -26,10 +34,11 @@ class BTPUser(Base): id: Column | int = Column(Integer, primary_key=True) user_id: Column | int = Column(BigInteger) - topic: Column | int = Column(Integer, ForeignKey(BTPTopic.id)) # TODO use relationship + topic_id: Column | int = Column(Integer, ForeignKey('btp_topic.id', ondelete='CASCADE')) + topic: BTPTopic = relationship("BTPTopic", back_populates="users", lazy="subquery", foreign_keys=[topic_id]) @staticmethod - async def create(user_id: int, topic: int) -> BTPUser: - row = BTPUser(user_id=user_id, topic=topic) + async def create(user_id: int, topic_id: int) -> BTPUser: + row = BTPUser(user_id=user_id, topic_id=topic_id) await db.add(row) return row diff --git a/general/betheprofessional/translations/en.yml b/general/betheprofessional/translations/en.yml index df24ed464..5e77446d2 100644 --- a/general/betheprofessional/translations/en.yml +++ b/general/betheprofessional/translations/en.yml @@ -35,11 +35,11 @@ log_topics_registered: many: "{cnt} **topics** have been **registered**: {topics}" topics_unregistered: - one: "Topic has been deleted successfully. :white_check_mark:" - many: "{cnt} topics have been deleted successfully. :white_check_mark:" + one: "Topic has been deleted successfully. :white_check_mark:\nAll child Topics have been removed as well." + many: "{cnt} topics have been deleted successfully. :white_check_mark:\nAll child Topics have been removed as well." log_topics_unregistered: - one: "The **topic** {topics} has been **removed**." - many: "{cnt} **topics** have been **removed**: {topics}" + one: "The **topic** {topics} has been **removed**.\nAll child Topics have been removed as well." + many: "{cnt} **topics** have been **removed**: {topics}\nAll child Topics have been removed as well." single_un_assign_help: "Hey, did you know, that you can assign multiple topics by using `.+ TOPIC1,TOPIC2,TOPIC3` and remove multiple topics the same way using `.-`?\nTo remove all your Topics you can use `.- *`" From e42986d341e1ac90765c8863fb499cf98feb2c79 Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Mon, 16 May 2022 21:28:46 +0200 Subject: [PATCH 60/68] fix after merge --- general/betheprofessional/cog.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/general/betheprofessional/cog.py b/general/betheprofessional/cog.py index 526a14897..3e8695098 100644 --- a/general/betheprofessional/cog.py +++ b/general/betheprofessional/cog.py @@ -1,9 +1,10 @@ import string -from discord import Member, Embed, Role -from discord.ext import commands +from discord import Member, Embed, Role, Message +from discord.ext import commands, tasks from discord.ext.commands import guild_only, Context, CommandError, UserInputError +import PyDrocsid from PyDrocsid.cog import Cog from PyDrocsid.command import reply, Confirmation from PyDrocsid.database import db, select, db_wrapper, filter_by, delete From 5523e6965fc3d89f762623cb5fd4edaee874eb2f Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Mon, 16 May 2022 21:39:47 +0200 Subject: [PATCH 61/68] added confirmation on topic unregister and fixed codestyle stuff --- general/betheprofessional/cog.py | 13 ++++--------- general/betheprofessional/models.py | 11 ++++------- general/betheprofessional/translations/en.yml | 1 + 3 files changed, 9 insertions(+), 16 deletions(-) diff --git a/general/betheprofessional/cog.py b/general/betheprofessional/cog.py index 3e8695098..4d1184ec5 100644 --- a/general/betheprofessional/cog.py +++ b/general/betheprofessional/cog.py @@ -40,7 +40,6 @@ async def split_parents(topics: list[str], assignable: bool) -> list[tuple[str, parents: list[BTPTopic | None | CommandError] = [ await db.first(filter_by(BTPTopic, name=topic)) - if await db.exists(filter_by(BTPTopic, name=topic)) else CommandError(t.parent_not_exists(topic)) for topic in topic_tree[:-1] @@ -158,9 +157,7 @@ async def list_topics(self, ctx: Context, parent_topic: str | None): [ f"`{topic.name}" + ( # noqa: W503 - f" ({c})`" - if (c := await db.count(filter_by(BTPTopic, parent_id=topic.id))) > 0 - else "`" + f" ({c})`" if (c := await db.count(filter_by(BTPTopic, parent_id=topic.id))) > 0 else "`" ) for topic in topics ] @@ -309,21 +306,19 @@ async def delete_topics(self, ctx: Context, *, topics: str): topics: list[str] = split_topics(topics) - # TODO: confirm message to delete (multiple) topic(s) - for topic in topics: if not await db.exists(filter_by(BTPTopic, name=topic)): raise CommandError(t.topic_not_registered(topic)) - #if not await Confirm(ctx.author, True, ) + if not await Confirmation(danger=True).run(ctx, t.confirm_delete_topics(topics=", ".join(topics))): + return for topic in topics: await db.exec(delete(BTPTopic).where(BTPTopic.name == topic)) embed = Embed(title=t.betheprofessional, colour=Colors.BeTheProfessional) embed.description = t.topics_unregistered(cnt=len(topics)) await send_to_changelog( - ctx.guild, - t.log_topics_unregistered(cnt=len(topics), topics=", ".join(f"`{t}`" for t in topics)), + ctx.guild, t.log_topics_unregistered(cnt=len(topics), topics=", ".join(f"`{t}`" for t in topics)) ) await send_long_embed(ctx, embed) diff --git a/general/betheprofessional/models.py b/general/betheprofessional/models.py index 799c97649..30adbca43 100644 --- a/general/betheprofessional/models.py +++ b/general/betheprofessional/models.py @@ -11,19 +11,16 @@ class BTPTopic(Base): id: Column | int = Column(Integer, primary_key=True) name: Column | str = Column(String(255), unique=True) - parent_id: Column | int = Column(Integer, ForeignKey('btp_topic.id', ondelete='CASCADE')) + parent_id: Column | int = Column(Integer, ForeignKey("btp_topic.id", ondelete="CASCADE")) children: list[BTPTopic] = relationship( - "BTPTopic", - backref=backref("parent", remote_side=id, foreign_keys=[parent_id]), - lazy="subquery", + "BTPTopic", backref=backref("parent", remote_side=id, foreign_keys=[parent_id]), lazy="subquery" ) role_id: Column | int = Column(BigInteger, unique=True) users: list[BTPUser] = relationship("BTPUser", back_populates="topic") assignable: Column | bool = Column(Boolean) @staticmethod - async def create( - name: str, role_id: int | None, assignable: bool, parent_id: int | None) -> BTPTopic: + async def create(name: str, role_id: int | None, assignable: bool, parent_id: int | None) -> BTPTopic: row = BTPTopic(name=name, role_id=role_id, parent_id=parent_id, assignable=assignable) await db.add(row) return row @@ -34,7 +31,7 @@ class BTPUser(Base): id: Column | int = Column(Integer, primary_key=True) user_id: Column | int = Column(BigInteger) - topic_id: Column | int = Column(Integer, ForeignKey('btp_topic.id', ondelete='CASCADE')) + topic_id: Column | int = Column(Integer, ForeignKey("btp_topic.id", ondelete="CASCADE")) topic: BTPTopic = relationship("BTPTopic", back_populates="users", lazy="subquery", foreign_keys=[topic_id]) @staticmethod diff --git a/general/betheprofessional/translations/en.yml b/general/betheprofessional/translations/en.yml index 5e77446d2..80563b274 100644 --- a/general/betheprofessional/translations/en.yml +++ b/general/betheprofessional/translations/en.yml @@ -37,6 +37,7 @@ log_topics_registered: topics_unregistered: one: "Topic has been deleted successfully. :white_check_mark:\nAll child Topics have been removed as well." many: "{cnt} topics have been deleted successfully. :white_check_mark:\nAll child Topics have been removed as well." +confirm_delete_topics: "Are you sure you want to delete the following topics?\n{topics}\n:warning: This will delete all child Topics as well. :warning:" log_topics_unregistered: one: "The **topic** {topics} has been **removed**.\nAll child Topics have been removed as well." many: "{cnt} **topics** have been **removed**: {topics}\nAll child Topics have been removed as well." From a91bccc9ee7f0b4cd803ad00b26112d0aaaa281b Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Mon, 16 May 2022 21:45:23 +0200 Subject: [PATCH 62/68] sorted imports --- general/betheprofessional/cog.py | 18 ++++++++++-------- general/betheprofessional/models.py | 6 +++--- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/general/betheprofessional/cog.py b/general/betheprofessional/cog.py index 4d1184ec5..3a305e5e0 100644 --- a/general/betheprofessional/cog.py +++ b/general/betheprofessional/cog.py @@ -1,25 +1,27 @@ import string -from discord import Member, Embed, Role, Message +from discord import Embed, Member, Message, Role from discord.ext import commands, tasks -from discord.ext.commands import guild_only, Context, CommandError, UserInputError +from discord.ext.commands import CommandError, Context, UserInputError, guild_only import PyDrocsid from PyDrocsid.cog import Cog -from PyDrocsid.command import reply, Confirmation -from PyDrocsid.database import db, select, db_wrapper, filter_by, delete +from PyDrocsid.command import Confirmation, reply +from PyDrocsid.database import db, db_wrapper, delete, filter_by, select from PyDrocsid.embeds import send_long_embed from PyDrocsid.environment import CACHE_TTL from PyDrocsid.logger import get_logger from PyDrocsid.redis import redis from PyDrocsid.translations import t from PyDrocsid.util import calculate_edit_distance + from .colors import Colors -from .models import BTPUser, BTPTopic +from .models import BTPTopic, BTPUser from .permissions import BeTheProfessionalPermission from .settings import BeTheProfessionalSettings from ...contributor import Contributor -from ...pubsub import send_to_changelog, send_alert +from ...pubsub import send_alert, send_to_changelog + tg = t.g t = t.betheprofessional @@ -126,7 +128,7 @@ async def list_topics(self, ctx: Context, parent_topic: str | None): None if parent_topic is None else await db.first(filter_by(BTPTopic, name=parent_topic)) - or CommandError(t.topic_not_found(parent_topic)) # noqa: W503 + or CommandError(t.topic_not_found(parent_topic)) # noqa: W503 ) if isinstance(parent, CommandError): raise parent @@ -178,7 +180,7 @@ async def assign_topics(self, ctx: Context, *, topics: str): topic for topic in await parse_topics(topics) if (await db.exists(filter_by(BTPTopic, id=topic.id))) - and not (await db.exists(filter_by(BTPUser, user_id=member.id, topic_id=topic.id))) # noqa: W503 + and not (await db.exists(filter_by(BTPUser, user_id=member.id, topic_id=topic.id))) # noqa: W503 ] roles: list[Role] = [] diff --git a/general/betheprofessional/models.py b/general/betheprofessional/models.py index 30adbca43..d1eab9d6d 100644 --- a/general/betheprofessional/models.py +++ b/general/betheprofessional/models.py @@ -1,9 +1,9 @@ from __future__ import annotations -from sqlalchemy.orm import relationship, backref +from sqlalchemy import BigInteger, Boolean, Column, ForeignKey, Integer, String +from sqlalchemy.orm import backref, relationship -from PyDrocsid.database import db, Base -from sqlalchemy import Column, BigInteger, Boolean, Integer, String, ForeignKey +from PyDrocsid.database import Base, db class BTPTopic(Base): From 0cea283c983ab469bf2255b421deba080207d8fc Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Mon, 16 May 2022 21:47:13 +0200 Subject: [PATCH 63/68] black+isort auto format --- general/betheprofessional/cog.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/general/betheprofessional/cog.py b/general/betheprofessional/cog.py index 3a305e5e0..8d1c1a50f 100644 --- a/general/betheprofessional/cog.py +++ b/general/betheprofessional/cog.py @@ -128,7 +128,7 @@ async def list_topics(self, ctx: Context, parent_topic: str | None): None if parent_topic is None else await db.first(filter_by(BTPTopic, name=parent_topic)) - or CommandError(t.topic_not_found(parent_topic)) # noqa: W503 + or CommandError(t.topic_not_found(parent_topic)) # noqa: W503 ) if isinstance(parent, CommandError): raise parent @@ -180,7 +180,7 @@ async def assign_topics(self, ctx: Context, *, topics: str): topic for topic in await parse_topics(topics) if (await db.exists(filter_by(BTPTopic, id=topic.id))) - and not (await db.exists(filter_by(BTPUser, user_id=member.id, topic_id=topic.id))) # noqa: W503 + and not (await db.exists(filter_by(BTPUser, user_id=member.id, topic_id=topic.id))) # noqa: W503 ] roles: list[Role] = [] From 055793a7ee86331da4581b1d370ba1f0c5daa78f Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Mon, 18 Jul 2022 15:53:27 +0200 Subject: [PATCH 64/68] added assignable parameter, rewrote update roles logic and improved code/fixed TODOs --- general/betheprofessional/cog.py | 65 ++++++++++++----------------- general/betheprofessional/models.py | 2 +- 2 files changed, 28 insertions(+), 39 deletions(-) diff --git a/general/betheprofessional/cog.py b/general/betheprofessional/cog.py index 8d1c1a50f..4667e8569 100644 --- a/general/betheprofessional/cog.py +++ b/general/betheprofessional/cog.py @@ -283,9 +283,11 @@ async def register_topics(self, ctx: Context, *, topic_paths: str, assignable: b registered_topics.append(topic) for registered_topic in registered_topics: - # TODO: assignable? await BTPTopic.create( - registered_topic[0], None, True, registered_topic[2][-1].id if len(registered_topic[2]) > 0 else None + registered_topic[0], + None, + assignable, + registered_topic[2][-1].id if len(registered_topic[2]) > 0 else None, ) embed = Embed(title=t.betheprofessional, colour=Colors.BeTheProfessional) @@ -503,7 +505,6 @@ async def user_topics(self, ctx: Context, member: Member | None): if member is None: member = ctx.author - # TODO use relationships and join topics_assignments: list[BTPUser] = await db.all(select(BTPUser).filter_by(user_id=member.id)) embed = Embed(title=t.betheprofessional, color=Colors.BeTheProfessional) @@ -541,44 +542,32 @@ async def update_roles(self): topic_count: dict[int, int] = {} # TODO rewrite from here.... - for topic in await db.all(select(BTPTopic)): - # TODO use relationship and join - topic_count[topic.id] = await db.count(select(BTPUser).filter_by(topic_id=topic.id)) - # not using dict.items() because of typing - # TODO Let db sort topics by count and then by - # TODO fix TODO ^^ - topic_count_items: list[tuple[int, int]] = list(zip(topic_count.keys(), topic_count.values())) - topic_count = dict(sorted(topic_count_items, key=lambda x: x[0])) - - # Sort Topics By Count, Keep only Topics with a Count of BeTheProfessionalSettings.RoleCreateMinUsers or above - # Limit Roles to BeTheProfessionalSettings.RoleLimit - top_topics: list[int] = list( - filter( - lambda topic_id: topic_count[topic_id] >= role_create_min_users, - sorted(topic_count, key=lambda x: topic_count[x], reverse=True), - ) - )[: await BeTheProfessionalSettings.RoleLimit.get()] + for topic in await db.all(select(BTPTopic).order_by(BTPTopic.id.asc())): + if len(topic.users) >= role_create_min_users: + topic_count[topic.id] = len(topic.users) + + # Sort Topics By Count and Limit Roles to BeTheProfessionalSettings.RoleLimit + top_topics: list[int] = sorted(topic_count, key=lambda x: topic_count[x], reverse=True)[ + : await BeTheProfessionalSettings.RoleLimit.get() + ] # TODO until here # Delete old Top Topic Roles - # TODO use filter_by - for topic in await db.all(select(BTPTopic).filter(BTPTopic.role_id is not None)): # type: BTPTopic - # TODO use sql "NOT IN" expression - if topic.id not in top_topics: - if topic.role_id is not None: - await self.bot.guilds[0].get_role(topic.role_id).delete() - topic.role_id = None + for topic in await db.all( + select(BTPTopic).filter(BTPTopic.role_id.is_not(None), BTPTopic.id.not_in(top_topics)) + ): # type: BTPTopic + await self.bot.guilds[0].get_role(topic.role_id).delete() + topic.role_id = None # Create new Topic Roles roles: dict[int, Role] = {} - # TODO use sql "IN" expression - for top_topic in top_topics: - topic: BTPTopic = await db.first(select(BTPTopic).filter_by(id=top_topic)) - if topic.role_id is None: - role = await self.bot.guilds[0].create_role(name=topic.name) - topic.role_id = role.id - roles[topic.id] = role + for topic in await db.all( + select(BTPTopic).filter(BTPTopic.id.in_(top_topics), BTPTopic.role_id.is_(None)) + ): # type: BTPTopic + role = await self.bot.guilds[0].create_role(name=topic.name) + topic.role_id = role.id + roles[topic.id] = role # Iterate over all members(with topics) and add the role to them # TODO add filter, only select topics with newly added roles @@ -597,8 +586,8 @@ async def update_roles(self): logger.info("Created Top Topic Roles") async def on_member_join(self, member: Member): - # TODO use relationship and join - topics: list[BTPUser] = await db.all(select(BTPUser).filter_by(user_id=member.id)) - role_ids: list[int] = [(await db.first(select(BTPTopic).filter_by(id=topic))).role_id for topic in topics] - roles: list[Role] = [self.bot.guilds[0].get_role(role_id) for role_id in role_ids] + roles: list[Role] = [ + self.bot.guilds[0].get_role(topic.role_id) + for topic in await db.all(select(BTPUser).filter_by(user_id=member.id)) + ] await member.add_roles(*roles, atomic=False) diff --git a/general/betheprofessional/models.py b/general/betheprofessional/models.py index d1eab9d6d..e668e0c2b 100644 --- a/general/betheprofessional/models.py +++ b/general/betheprofessional/models.py @@ -16,7 +16,7 @@ class BTPTopic(Base): "BTPTopic", backref=backref("parent", remote_side=id, foreign_keys=[parent_id]), lazy="subquery" ) role_id: Column | int = Column(BigInteger, unique=True) - users: list[BTPUser] = relationship("BTPUser", back_populates="topic") + users: list[BTPUser] = relationship("BTPUser", back_populates="topic", lazy="subquery") assignable: Column | bool = Column(Boolean) @staticmethod From bab177066f58599c0f15359d8e081119896554c1 Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Mon, 18 Jul 2022 17:33:46 +0200 Subject: [PATCH 65/68] Changed BTP Settings --- general/betheprofessional/cog.py | 18 ++++++++---------- general/betheprofessional/translations/en.yml | 12 ++++-------- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/general/betheprofessional/cog.py b/general/betheprofessional/cog.py index 4667e8569..79b685f1c 100644 --- a/general/betheprofessional/cog.py +++ b/general/betheprofessional/cog.py @@ -93,7 +93,7 @@ async def get_topics() -> list[BTPTopic]: async def change_setting(ctx: Context, name: str, value: any): data = t.settings[name] - await getattr(BeTheProfessionalSettings, data["internal_name"]).set(value) + await getattr(BeTheProfessionalSettings, name).set(value) embed = Embed(title=t.betheprofessional, color=Colors.green) embed.description = data["updated"].format(value) @@ -361,10 +361,11 @@ async def btp(self, ctx: Context): embed = Embed(title=t.betheprofessional, color=Colors.BeTheProfessional) # TODO do not do that!!!! - for setting_item in t.settings.__dict__["_fallback"].keys(): + + for setting_item in ["RoleLimit", "RoleCreateMinUsers", "LeaderboardDefaultN", "LeaderboardMaxN"]: data = getattr(t.settings, setting_item) embed.add_field( - name=data.name, value=await getattr(BeTheProfessionalSettings, data.internal_name).get(), inline=False + name=data.name, value=await getattr(BeTheProfessionalSettings, setting_item).get(), inline=False ) await reply(ctx, embed=embed) @@ -378,7 +379,7 @@ async def role_limit(self, ctx: Context, role_limit: int): if role_limit <= 0: raise CommandError(t.must_be_above_zero(t.settings.role_limit.name)) - await change_setting(ctx, "role_limit", role_limit) + await change_setting(ctx, "RoleLimit", role_limit) @btp.command() @guild_only() @@ -390,7 +391,7 @@ async def role_create_min_users(self, ctx: Context, role_create_min_users: int): if role_create_min_users < 0: raise CommandError(t.must_be_zero_or_above(t.settings.role_create_min_users.name)) - await change_setting(ctx, "role_create_min_users", role_create_min_users) + await change_setting(ctx, "RoleCreateMinUsers", role_create_min_users) @btp.command() @guild_only() @@ -402,7 +403,7 @@ async def leaderboard_default_n(self, ctx: Context, leaderboard_default_n: int): if leaderboard_default_n <= 0: raise CommandError(t.must_be_above_zero(t.settings.leaderboard_default_n.name)) - await change_setting(ctx, "leaderboard_default_n", leaderboard_default_n) + await change_setting(ctx, "LeaderboardDefaultN", leaderboard_default_n) @btp.command() @guild_only() @@ -414,7 +415,7 @@ async def leaderboard_max_n(self, ctx: Context, leaderboard_max_n: int): if leaderboard_max_n <= 0: raise CommandError(t.must_be_above_zero(t.settings.leaderboard_max_n.name)) - await change_setting(ctx, "leaderboard_max_n", leaderboard_max_n) + await change_setting(ctx, "LeaderboardMaxN", leaderboard_max_n) @btp.command(aliases=["lb"]) @guild_only() @@ -541,7 +542,6 @@ async def update_roles(self): logger.info("Started Update Role Loop") topic_count: dict[int, int] = {} - # TODO rewrite from here.... for topic in await db.all(select(BTPTopic).order_by(BTPTopic.id.asc())): if len(topic.users) >= role_create_min_users: topic_count[topic.id] = len(topic.users) @@ -551,8 +551,6 @@ async def update_roles(self): : await BeTheProfessionalSettings.RoleLimit.get() ] - # TODO until here - # Delete old Top Topic Roles for topic in await db.all( select(BTPTopic).filter(BTPTopic.role_id.is_not(None), BTPTopic.id.not_in(top_topics)) diff --git a/general/betheprofessional/translations/en.yml b/general/betheprofessional/translations/en.yml index 80563b274..84e08831b 100644 --- a/general/betheprofessional/translations/en.yml +++ b/general/betheprofessional/translations/en.yml @@ -69,19 +69,15 @@ must_be_above_zero: "`{}` must be above zero!" must_be_zero_or_above: "`{}` must be zero or above!" settings: - role_limit: + RoleLimit: name: "Role Limit" - internal_name: "RoleLimit" updated: "The BTP Role Limit is now `{}`" - role_create_min_users: + RoleCreateMinUsers: name: "Role Create Min Users" - internal_name: "RoleCreateMinUsers" updated: "Role Create Min Users Limit is now `{}`" - leaderboard_default_n: + LeaderboardDefaultN: name: "Leaderboard Default N" - internal_name: "LeaderboardDefaultN" updated: "Leaderboard Default N is now `{}`" - leaderboard_max_n: + LeaderboardMaxN: name: "Leaderboard Max N" - internal_name: "LeaderboardMaxN" updated: "Leaderboard Max N is now `{}`" From a2a0a91b2a8f3cc5891ca286bcc81090a8417b79 Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Mon, 18 Jul 2022 20:49:29 +0200 Subject: [PATCH 66/68] fixed linter --- general/betheprofessional/cog.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/general/betheprofessional/cog.py b/general/betheprofessional/cog.py index 79b685f1c..a49b9a604 100644 --- a/general/betheprofessional/cog.py +++ b/general/betheprofessional/cog.py @@ -68,11 +68,9 @@ async def parse_topics(topics_str: str) -> list[BTPTopic]: topic = await db.first(filter_by(BTPTopic, name=topic_name)) if topic is None and len(all_topics) > 0: - - def dist(name: str) -> int: - return calculate_edit_distance(name.lower(), topic_name.lower()) - - best_dist, best_match = min((dist(r.name), r.name) for r in all_topics) + best_dist, best_match = min( + (calculate_edit_distance(r.name.lower(), topic_name.lower()), r.name) for r in all_topics + ) if best_dist <= 5: raise CommandError(t.topic_not_found_did_you_mean(topic_name, best_match)) @@ -341,7 +339,7 @@ async def topic(self, ctx: Context, topic_name: str, message: Message | None): mention = f"<@&{topic.role_id}>" else: topic_members: list[BTPUser] = await db.all(select(BTPUser).filter_by(topic_id=topic.id)) - mention = ", ".join(map(lambda m: f"<@{m.user_id}>", topic_members)) + mention = ", ".join([f"<@{m.user_id}>" for m in topic_members]) if mention == "": raise CommandError(t.nobody_has_topic(topic_name)) From 3ff8eba59f9e35e329e5eabd14595396ed19f2b2 Mon Sep 17 00:00:00 2001 From: Tert0 <62036464+Tert0@users.noreply.github.com> Date: Mon, 18 Jul 2022 21:31:28 +0200 Subject: [PATCH 67/68] resolved todos --- general/betheprofessional/cog.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/general/betheprofessional/cog.py b/general/betheprofessional/cog.py index a49b9a604..cd9be8a83 100644 --- a/general/betheprofessional/cog.py +++ b/general/betheprofessional/cog.py @@ -358,7 +358,6 @@ async def btp(self, ctx: Context): return embed = Embed(title=t.betheprofessional, color=Colors.BeTheProfessional) - # TODO do not do that!!!! for setting_item in ["RoleLimit", "RoleCreateMinUsers", "LeaderboardDefaultN", "LeaderboardMaxN"]: data = getattr(t.settings, setting_item) @@ -549,6 +548,8 @@ async def update_roles(self): : await BeTheProfessionalSettings.RoleLimit.get() ] + new_roles_topic_id: list[int] = [] + # Delete old Top Topic Roles for topic in await db.all( select(BTPTopic).filter(BTPTopic.role_id.is_not(None), BTPTopic.id.not_in(top_topics)) @@ -561,13 +562,15 @@ async def update_roles(self): for topic in await db.all( select(BTPTopic).filter(BTPTopic.id.in_(top_topics), BTPTopic.role_id.is_(None)) ): # type: BTPTopic + new_roles_topic_id.append(topic.id) role = await self.bot.guilds[0].create_role(name=topic.name) topic.role_id = role.id roles[topic.id] = role # Iterate over all members(with topics) and add the role to them - # TODO add filter, only select topics with newly added roles - member_ids: set[int] = {btp_user.user_id for btp_user in await db.all(select(BTPUser))} + member_ids: set[int] = { + btp_user.user_id for btp_user in await db.all(select(BTPUser)) if btp_user.topic.id in new_roles_topic_id + } for member_id in member_ids: member: Member = self.bot.guilds[0].get_member(member_id) if member is None: @@ -575,8 +578,7 @@ async def update_roles(self): member_roles: list[Role] = [ roles.get(btp_user.topic) for btp_user in await db.all(select(BTPUser).filter_by(user_id=member_id)) ] - # TODO use filter or something? - member_roles = [item for item in member_roles if item is not None] + member_roles = list(filter(lambda x: x is not None, member_roles)) await member.add_roles(*member_roles, atomic=False) logger.info("Created Top Topic Roles") From 1924e3ffba6d385cb94c294f6049a5169bdd809d Mon Sep 17 00:00:00 2001 From: TheCataliasTNT2k <44349750+TheCataliasTNT2k@users.noreply.github.com> Date: Mon, 25 Jul 2022 22:06:42 +0200 Subject: [PATCH 68/68] added a few todos --- general/betheprofessional/cog.py | 157 +++++++++++++++---------------- 1 file changed, 76 insertions(+), 81 deletions(-) diff --git a/general/betheprofessional/cog.py b/general/betheprofessional/cog.py index cd9be8a83..685ed811f 100644 --- a/general/betheprofessional/cog.py +++ b/general/betheprofessional/cog.py @@ -32,32 +32,28 @@ def split_topics(topics: str) -> list[str]: + # TODO docstring return [topic for topic in map(str.strip, topics.replace(";", ",").split(",")) if topic] -async def split_parents(topics: list[str], assignable: bool) -> list[tuple[str, bool, list[BTPTopic]] | None]: +async def split_parents(topics: list[str], assignable: bool) -> list[tuple[str, bool, list[BTPTopic]]]: + # TODO docstring result: list[tuple[str, bool, list[BTPTopic]] | None] = [] for topic in topics: topic_tree = topic.split("/") - parents: list[BTPTopic | None | CommandError] = [ - await db.first(filter_by(BTPTopic, name=topic)) - if await db.exists(filter_by(BTPTopic, name=topic)) - else CommandError(t.parent_not_exists(topic)) - for topic in topic_tree[:-1] - ] - - parents = [parent for parent in parents if parent is not None] - for parent in parents: - if isinstance(parent, CommandError): - raise parent + parents: list[BTPTopic | None] = [] + for par in topic_tree[:-1]: + parents.append(parent := await db.first(filter_by(BTPTopic, name=par))) # TODO redis? + if parent is None: + raise CommandError(t.parent_not_exists(topic)) - topic = topic_tree[-1] - result.append((topic, assignable, parents)) + result.append((topic_tree[-1], assignable, parents)) return result async def parse_topics(topics_str: str) -> list[BTPTopic]: + # TODO docstring topics: list[BTPTopic] = [] all_topics: list[BTPTopic] = await get_topics() @@ -65,7 +61,7 @@ async def parse_topics(topics_str: str) -> list[BTPTopic]: raise CommandError(t.no_topics_registered) for topic_name in split_topics(topics_str): - topic = await db.first(filter_by(BTPTopic, name=topic_name)) + topic = await db.first(filter_by(BTPTopic, name=topic_name)) # TODO db obsolete if topic is None and len(all_topics) > 0: best_dist, best_match = min( @@ -83,13 +79,12 @@ async def parse_topics(topics_str: str) -> list[BTPTopic]: async def get_topics() -> list[BTPTopic]: - topics: list[BTPTopic] = [] - async for topic in await db.stream(select(BTPTopic)): - topics.append(topic) - return topics + # TODO docstring + return await db.all(select(BTPTopic)) async def change_setting(ctx: Context, name: str, value: any): + # TODO docstring data = t.settings[name] await getattr(BeTheProfessionalSettings, name).set(value) @@ -116,6 +111,65 @@ async def on_ready(self): except RuntimeError: self.update_roles.restart() + @tasks.loop(hours=24) + @db_wrapper + async def update_roles(self): + role_create_min_users = await BeTheProfessionalSettings.RoleCreateMinUsers.get() + + logger.info("Started Update Role Loop") + topic_count: dict[int, int] = {} + + for topic in await db.all(select(BTPTopic).order_by(BTPTopic.id.asc())): + if len(topic.users) >= role_create_min_users: + topic_count[topic.id] = len(topic.users) + + # Sort Topics By Count and Limit Roles to BeTheProfessionalSettings.RoleLimit + top_topics: list[int] = sorted(topic_count, key=lambda x: topic_count[x], reverse=True)[ + : await BeTheProfessionalSettings.RoleLimit.get() + ] + + new_roles_topic_id: list[int] = [] + + # Delete old Top Topic Roles + for topic in await db.all( + select(BTPTopic).filter(BTPTopic.role_id.is_not(None), BTPTopic.id.not_in(top_topics)) + ): # type: BTPTopic + await self.bot.guilds[0].get_role(topic.role_id).delete() + topic.role_id = None + + # Create new Topic Roles + roles: dict[int, Role] = {} + for topic in await db.all( + select(BTPTopic).filter(BTPTopic.id.in_(top_topics), BTPTopic.role_id.is_(None)) + ): # type: BTPTopic + new_roles_topic_id.append(topic.id) + role = await self.bot.guilds[0].create_role(name=topic.name) + topic.role_id = role.id + roles[topic.id] = role + + # Iterate over all members(with topics) and add the role to them + member_ids: set[int] = { + btp_user.user_id for btp_user in await db.all(select(BTPUser)) if btp_user.topic.id in new_roles_topic_id + } + for member_id in member_ids: + member: Member = self.bot.guilds[0].get_member(member_id) + if member is None: + continue + member_roles: list[Role] = [ + roles.get(btp_user.topic) for btp_user in await db.all(select(BTPUser).filter_by(user_id=member_id)) + ] + member_roles = list(filter(lambda x: x is not None, member_roles)) + await member.add_roles(*member_roles, atomic=False) + + logger.info("Created Top Topic Roles") + + async def on_member_join(self, member: Member): + roles: list[Role] = [ + self.bot.guilds[0].get_role(topic.role_id) + async for topic in await db.stream(select(BTPUser).filter_by(user_id=member.id)) + ] + await member.add_roles(*roles, atomic=False) + @commands.command(name="?") @guild_only() async def list_topics(self, ctx: Context, parent_topic: str | None): @@ -177,7 +231,7 @@ async def assign_topics(self, ctx: Context, *, topics: str): topics: list[BTPTopic] = [ topic for topic in await parse_topics(topics) - if (await db.exists(filter_by(BTPTopic, id=topic.id))) + if (await db.exists(filter_by(BTPTopic, id=topic.id))) # TODO db obsolete and not (await db.exists(filter_by(BTPUser, user_id=member.id, topic_id=topic.id))) # noqa: W503 ] @@ -263,14 +317,14 @@ async def register_topics(self, ctx: Context, *, topic_paths: str, assignable: b """ names = split_topics(topic_paths) - topic_paths: list[tuple[str, bool, list[BTPTopic] | None]] = await split_parents(names, assignable) + topic_paths: list[tuple[str, bool, list[BTPTopic]]] = await split_parents(names, assignable) if not names or not topic_paths: raise UserInputError valid_chars = set(string.ascii_letters + string.digits + " !#$%&'()+-./:<=>?[\\]^_`{|}~") registered_topics: list[tuple[str, bool, list[BTPTopic]] | None] = [] for topic in topic_paths: - if len(topic) > 100: + if len(topic) > 100: # TODO raise CommandError(t.topic_too_long(topic)) if any(c not in valid_chars for c in topic[0]): raise CommandError(t.topic_invalid_chars(topic)) @@ -530,62 +584,3 @@ async def topic_update_roles(self, ctx: Context): await self.update_roles() await reply(ctx, "Updated Topic Roles") - - @tasks.loop(hours=24) - @db_wrapper - async def update_roles(self): - role_create_min_users = await BeTheProfessionalSettings.RoleCreateMinUsers.get() - - logger.info("Started Update Role Loop") - topic_count: dict[int, int] = {} - - for topic in await db.all(select(BTPTopic).order_by(BTPTopic.id.asc())): - if len(topic.users) >= role_create_min_users: - topic_count[topic.id] = len(topic.users) - - # Sort Topics By Count and Limit Roles to BeTheProfessionalSettings.RoleLimit - top_topics: list[int] = sorted(topic_count, key=lambda x: topic_count[x], reverse=True)[ - : await BeTheProfessionalSettings.RoleLimit.get() - ] - - new_roles_topic_id: list[int] = [] - - # Delete old Top Topic Roles - for topic in await db.all( - select(BTPTopic).filter(BTPTopic.role_id.is_not(None), BTPTopic.id.not_in(top_topics)) - ): # type: BTPTopic - await self.bot.guilds[0].get_role(topic.role_id).delete() - topic.role_id = None - - # Create new Topic Roles - roles: dict[int, Role] = {} - for topic in await db.all( - select(BTPTopic).filter(BTPTopic.id.in_(top_topics), BTPTopic.role_id.is_(None)) - ): # type: BTPTopic - new_roles_topic_id.append(topic.id) - role = await self.bot.guilds[0].create_role(name=topic.name) - topic.role_id = role.id - roles[topic.id] = role - - # Iterate over all members(with topics) and add the role to them - member_ids: set[int] = { - btp_user.user_id for btp_user in await db.all(select(BTPUser)) if btp_user.topic.id in new_roles_topic_id - } - for member_id in member_ids: - member: Member = self.bot.guilds[0].get_member(member_id) - if member is None: - continue - member_roles: list[Role] = [ - roles.get(btp_user.topic) for btp_user in await db.all(select(BTPUser).filter_by(user_id=member_id)) - ] - member_roles = list(filter(lambda x: x is not None, member_roles)) - await member.add_roles(*member_roles, atomic=False) - - logger.info("Created Top Topic Roles") - - async def on_member_join(self, member: Member): - roles: list[Role] = [ - self.bot.guilds[0].get_role(topic.role_id) - for topic in await db.all(select(BTPUser).filter_by(user_id=member.id)) - ] - await member.add_roles(*roles, atomic=False)