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
 .+ <topic>
 ```
 
-|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
 .- <topic>
 ```
 
-|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
 .* <topic>
 ```
 
-|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
 ./ <topic>
 ```
 
-|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>
+.topic <topic_name> [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
 .* <topic>
@@ -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 <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 <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 <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 <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
 .+ <topic>
 ```
+
 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)