From e5df660cbd9c4f1bfba54c738be45f600c4d1587 Mon Sep 17 00:00:00 2001
From: NekoFanatic <83883849+NekoFanatic@users.noreply.github.com>
Date: Mon, 11 Apr 2022 12:31:12 +0200
Subject: [PATCH 01/44] saving changes
---
general/polls/cog.py | 62 ++++++++++++++---
general/polls/models.py | 107 ++++++++++++++++++++++++++++++
general/polls/permissions.py | 2 +
general/polls/settings.py | 9 +++
general/polls/translations/en.yml | 28 ++++++++
5 files changed, 200 insertions(+), 8 deletions(-)
create mode 100644 general/polls/models.py
create mode 100644 general/polls/settings.py
diff --git a/general/polls/cog.py b/general/polls/cog.py
index de1d278ac..71dfe99c7 100644
--- a/general/polls/cog.py
+++ b/general/polls/cog.py
@@ -4,11 +4,12 @@
from discord import Embed, Forbidden, Guild, Member, Message, PartialEmoji
from discord.ext import commands
-from discord.ext.commands import CommandError, Context, guild_only
+from discord.ext.commands import CommandError, Context, UserInputError, guild_only
from discord.utils import utcnow
from PyDrocsid.cog import Cog
-from PyDrocsid.embeds import EmbedLimits
+from PyDrocsid.command import docs
+from PyDrocsid.embeds import EmbedLimits, send_long_embed
from PyDrocsid.emojis import emoji_to_name, name_to_emoji
from PyDrocsid.events import StopEventHandling
from PyDrocsid.settings import RoleSettings
@@ -16,7 +17,9 @@
from PyDrocsid.util import check_wastebasket, is_teamler
from .colors import Colors
+from .models import RolesWeights
from .permissions import PollsPermission
+from .settings import PollsDefaultSettings
from ...contributor import Contributor
@@ -77,7 +80,13 @@ async def send_poll(
class PollsCog(Cog, name="Polls"):
- CONTRIBUTORS = [Contributor.MaxiHuHe04, Contributor.Defelo, Contributor.TNT2k, Contributor.wolflu]
+ CONTRIBUTORS = [
+ Contributor.MaxiHuHe04,
+ Contributor.Defelo,
+ Contributor.TNT2k,
+ Contributor.wolflu,
+ Contributor.NekoFanatic,
+ ]
def __init__(self, team_roles: list[str]):
self.team_roles: list[str] = team_roles
@@ -155,15 +164,52 @@ async def on_raw_reaction_remove(self, message: Message, _, member: Member):
await message.edit(embed=embed)
return
- @commands.command(usage=t.poll_usage, aliases=["vote"])
+ @commands.group(name="poll", aliases=["vote"])
@guild_only()
- async def poll(self, ctx: Context, *, args: str):
- """
- Starts a poll. Multiline options can be specified using a `\\` at the end of a line
- """
+ @docs(t.commands.poll)
+ async def poll(self, ctx: Context):
+ if not ctx.subcommand_passed:
+ raise UserInputError
+
+ @poll.command(name="quick", usage=t.poll_usage, aliases=["q"])
+ @docs(t.commands.quick)
+ async def quick(self, ctx: Context, *, args: str):
await send_poll(ctx, t.poll, args)
+ @poll.group(name="settings", aliases=["s"])
+ @PollsPermission.read.check
+ @docs(t.commands.settings)
+ async def settings(self, ctx: Context):
+ if ctx.subcommand_passed is not None:
+ if ctx.invoked_subcommand is None:
+ raise UserInputError
+ return
+
+ embed = Embed(title=t.poll_config.title, color=Colors.Polls)
+ time: int = await PollsDefaultSettings.duration.get()
+ embed.add_field(
+ name=t.poll_config.duration.name,
+ value=t.poll_config.duration.time(time) if not time <= 0 else t.poll_config.duration.unlimited,
+ inline=False,
+ )
+ choice: int = await PollsDefaultSettings.max_choices.get()
+ embed.add_field(
+ name=t.poll_config.choices.name,
+ value=t.poll_config.choices.amount(choice) if not choice <= 0 else t.poll_config.choices.unlimited,
+ inline=False,
+ )
+ hide: bool = await PollsDefaultSettings.hidden.get()
+ embed.add_field(name=t.poll_config.hidden.name, value=str(hide), inline=False)
+ roles = await RolesWeights.get()
+ everyone: int = await PollsDefaultSettings.everyone_power.get()
+ base: str = t.poll_config.roles.row(ctx.guild.default_role, everyone)
+ if roles:
+ base.join([t.poll_config.roles.row(role.role_id, role.weight) for role in roles])
+ embed.add_field(name=t.poll_config.roles.name, value=base, inline=False)
+
+ await send_long_embed(ctx, embed, paginate=False)
+
@commands.command(usage=t.poll_usage, aliases=["teamvote", "tp"])
@PollsPermission.team_poll.check
@guild_only()
diff --git a/general/polls/models.py b/general/polls/models.py
new file mode 100644
index 000000000..baae05700
--- /dev/null
+++ b/general/polls/models.py
@@ -0,0 +1,107 @@
+from __future__ import annotations
+
+from datetime import datetime
+from typing import Optional, Union
+
+from discord.utils import utcnow
+from sqlalchemy import BigInteger, Boolean, Column, Float, ForeignKey, Text
+from sqlalchemy.orm import relationship
+
+from PyDrocsid.database import Base, UTCDateTime, db, select
+
+
+# tabelle für vote stimmen (ForeignKey)
+# konfigutierbar ausschluss der mute-rolle
+# userpolls abspecken
+# default werte in settings
+# wizzard weg (alles eine zeile)
+# yn so lassen
+
+
+class Poll:
+ def __init__(self, owner: int, channel: int):
+ self.owner: int = owner
+ self.channel: int = channel
+ self.message_id: int = 0
+
+ self.question: str = ""
+ self.type: str = "standard" # standard, team
+ self.options: list[tuple[str, str]] = [] # [(emote, option), ...]
+ self.max_votes: int = 1
+ self.voted: dict[str, tuple[list[int], int]] = {} # {"user1": ([option_number, ...], weight), ...}
+ self.votes: dict[str, int] # {option: number_of_votes, ...}
+ self.roles: dict[str, float] = {} # {"role_id": weight, ...}
+ self.hidden: bool = False
+ self.duration: Optional[datetime] = None
+ self.active: bool = False
+
+
+class Polls(Base):
+ __tablename__ = "polls"
+
+ id: Union[Column, int] = Column(BigInteger, primary_key=True, autoincrement=True, unique=True)
+
+ options: list[Options] = relationship("Options", back_populates="poll")
+
+ message_id: Union[Column, int] = Column(BigInteger, unique=True)
+ poll_channel: Union[Column, int] = Column(BigInteger)
+ owner_id: Union[Column, int] = Column(BigInteger)
+ timestamp: Union[Column, datetime] = Column(UTCDateTime)
+ title: Union[Column, str] = Column(Text(256))
+ poll_type: Union[Column, str] = Column(Text(50))
+ end_time: Union[Column, datetime] = Column(UTCDateTime)
+ hidden_votes: Union[Column, bool] = Column(Boolean)
+ votes_amount: Union[Column, int] = Column(BigInteger)
+ poll_open: Union[Column, bool] = Column(Boolean)
+ can_delete: Union[Column, bool] = Column(Boolean)
+ keep: Union[Column, bool] = Column(Boolean)
+
+
+class Options(Base):
+ __tablename__ = "poll_options"
+
+ id: Union[Column, int] = Column(
+ BigInteger, ForeignKey("voted_user.option_id"), primary_key=True, autoincrement=True, unique=True
+ )
+ poll_id: Union[Column, int] = Column(BigInteger, ForeignKey("polls.id"))
+ emote: Union[Column, str] = Column(Text(30))
+ option: Union[Column, str] = Column(Text(150))
+
+ @staticmethod
+ async def create(poll: int, emote: str, option_text: str) -> Options:
+ options = Options(poll_id=poll, emote=emote, option=option_text)
+ await db.add(options)
+
+ return options
+
+
+class RolesWeights(Base):
+ __tablename__ = "roles_weight"
+
+ id: Union[Column, int] = Column(BigInteger, primary_key=True, autoincrement=True, unique=True)
+ role_id: Union[Column, int] = Column(BigInteger, unique=True)
+ weight: Union[Column, float] = Column(Float)
+ timestamp: Union[Column, datetime] = Column(UTCDateTime)
+
+ @staticmethod
+ async def create(role: int, weight: float) -> RolesWeights:
+ roles_weights = RolesWeights(role_id=role, weight=weight, timestamp=utcnow())
+ await db.add(roles_weights)
+
+ return roles_weights
+
+ async def remove(self) -> None:
+ await db.delete(self)
+
+ @staticmethod
+ async def get() -> list[RolesWeights]:
+ return await db.all(select(RolesWeights))
+
+
+class Voted(Base):
+ __tablename__ = "voted_user"
+
+ id: Union[Column, int] = Column(BigInteger, primary_key=True, autoincrement=True, unique=True)
+ user_id: Union[Column, int] = Column(BigInteger)
+ option_id: Options = relationship("Options")
+ vote_weight: Union[Column, float] = Column(Float)
diff --git a/general/polls/permissions.py b/general/polls/permissions.py
index 5195de9d3..6b5384d48 100644
--- a/general/polls/permissions.py
+++ b/general/polls/permissions.py
@@ -10,4 +10,6 @@ def description(self) -> str:
return t.polls.permissions[self.name]
team_poll = auto()
+ read = auto()
+ write = auto()
delete = auto()
diff --git a/general/polls/settings.py b/general/polls/settings.py
new file mode 100644
index 000000000..69f881d01
--- /dev/null
+++ b/general/polls/settings.py
@@ -0,0 +1,9 @@
+from PyDrocsid.settings import Settings
+
+
+class PollsDefaultSettings(Settings):
+ duration = 0 # 0 for unlimited duration (duration in hours)
+ max_choices = 0 # 0 for unlimited choices
+ type = "standard"
+ hidden = False
+ everyone_power = 1.0
diff --git a/general/polls/translations/en.yml b/general/polls/translations/en.yml
index e779d9255..2d81f3459 100644
--- a/general/polls/translations/en.yml
+++ b/general/polls/translations/en.yml
@@ -1,7 +1,35 @@
+commands:
+ poll: poll commands
+ quick: small poll with default options
+ new: advanced poll with more options
+ settings: poll settings
+
permissions:
team_poll: start a team poll
+ read: read poll configuration
+ write: edit poll configuration
delete: delete polls
+poll_config:
+ title: Default poll configuration
+ duration:
+ name: "**Duration**"
+ time:
+ one: "{} hour"
+ many: "{} hours"
+ unlimited: unlimited
+ choices:
+ name: "**Choices per user**"
+ amount:
+ one: "{} choice per user"
+ many: "{} choices per user"
+ unlimited: unlimited
+ hidden:
+ name: "**Hidden Votes**"
+ roles:
+ name: "**Role Weights**"
+ row: "\n{} -> `{}x`"
+
poll: Poll
team_poll: Team Poll
vote_explanation: Vote using the reactions below!
From b353e9565e66407d315b7b9522267e0e6cf22018 Mon Sep 17 00:00:00 2001
From: NekoFanatic <83883849+NekoFanatic@users.noreply.github.com>
Date: Mon, 11 Apr 2022 15:27:45 +0200
Subject: [PATCH 02/44] more code
---
general/polls/cog.py | 82 ++++++++++++++++++++++++++++---
general/polls/models.py | 18 +++----
general/polls/translations/en.yml | 41 ++++++++++++++--
3 files changed, 120 insertions(+), 21 deletions(-)
diff --git a/general/polls/cog.py b/general/polls/cog.py
index 71dfe99c7..cee791df7 100644
--- a/general/polls/cog.py
+++ b/general/polls/cog.py
@@ -2,13 +2,14 @@
import string
from typing import Optional, Tuple
-from discord import Embed, Forbidden, Guild, Member, Message, PartialEmoji
+from discord import Embed, Forbidden, Guild, Member, Message, PartialEmoji, Role
from discord.ext import commands
from discord.ext.commands import CommandError, Context, UserInputError, guild_only
from discord.utils import utcnow
from PyDrocsid.cog import Cog
-from PyDrocsid.command import docs
+from PyDrocsid.command import add_reactions, docs
+from PyDrocsid.database import db
from PyDrocsid.embeds import EmbedLimits, send_long_embed
from PyDrocsid.emojis import emoji_to_name, name_to_emoji
from PyDrocsid.events import StopEventHandling
@@ -21,6 +22,7 @@
from .permissions import PollsPermission
from .settings import PollsDefaultSettings
from ...contributor import Contributor
+from ...pubsub import send_to_changelog
tg = t.g
@@ -166,20 +168,20 @@ async def on_raw_reaction_remove(self, message: Message, _, member: Member):
@commands.group(name="poll", aliases=["vote"])
@guild_only()
- @docs(t.commands.poll)
+ @docs(t.commands.poll.poll)
async def poll(self, ctx: Context):
if not ctx.subcommand_passed:
raise UserInputError
@poll.command(name="quick", usage=t.poll_usage, aliases=["q"])
- @docs(t.commands.quick)
+ @docs(t.commands.poll.quick)
async def quick(self, ctx: Context, *, args: str):
await send_poll(ctx, t.poll, args)
@poll.group(name="settings", aliases=["s"])
@PollsPermission.read.check
- @docs(t.commands.settings)
+ @docs(t.commands.poll.settings.settings)
async def settings(self, ctx: Context):
if ctx.subcommand_passed is not None:
if ctx.invoked_subcommand is None:
@@ -203,13 +205,79 @@ async def settings(self, ctx: Context):
embed.add_field(name=t.poll_config.hidden.name, value=str(hide), inline=False)
roles = await RolesWeights.get()
everyone: int = await PollsDefaultSettings.everyone_power.get()
- base: str = t.poll_config.roles.row(ctx.guild.default_role, everyone)
+ base: str = t.poll_config.roles.ev_row(ctx.guild.default_role, everyone)
if roles:
- base.join([t.poll_config.roles.row(role.role_id, role.weight) for role in roles])
+ base += "".join([t.poll_config.roles.row(role.role_id, role.weight) for role in roles])
embed.add_field(name=t.poll_config.roles.name, value=base, inline=False)
await send_long_embed(ctx, embed, paginate=False)
+ @settings.command(name="roles_weights", aliases=["rw"])
+ @PollsPermission.write.check
+ @docs(t.commands.poll.settings.roles_weights)
+ async def roles_weights(self, ctx: Context, role: Role, weight: float = None):
+ element = await db.get(RolesWeights, role_id=role.id)
+
+ if not weight and not element:
+ raise CommandError(t.error.cant_set_weight)
+
+ if weight and weight < 0.1:
+ raise CommandError(t.error.weight_too_small)
+
+ if element and weight:
+ element.weight = weight
+ msg: str = t.role_weight.set(role.id, weight)
+ elif weight and not element:
+ await RolesWeights.create(role.id, weight)
+ msg: str = t.role_weight.set(role.id, weight)
+ else:
+ await element.remove()
+ msg: str = t.role_weight.reset(role.id)
+
+ await add_reactions(ctx.message, "white_check_mark")
+ await send_to_changelog(ctx.guild, msg)
+
+ @settings.command(name="duration", aliases=["d"])
+ @PollsPermission.write.check
+ @docs(t.commands.poll.settings.duration)
+ async def duration(self, ctx: Context, hours: int = None):
+ if not hours:
+ hours = 0
+ msg: str = t.duration.reset()
+ else:
+ msg: str = t.duration.set(hours)
+
+ await PollsDefaultSettings.duration.set(hours)
+ await add_reactions(ctx.message, "white_check_mark")
+ await send_to_changelog(ctx.guild, msg)
+
+ @settings.command(name="votes", aliases=["v", "choices", "c"])
+ @PollsPermission.write.check
+ @docs(t.commands.poll.settings.votes)
+ async def votes(self, ctx: Context, votes: int = None):
+ if not votes:
+ votes = 0
+ msg: str = t.votes.reset
+ else:
+ msg: str = t.votes.set(votes)
+
+ await PollsDefaultSettings.max_choices.set(votes)
+ await add_reactions(ctx.message, "white_check_mark")
+ await send_to_changelog(ctx.guild, msg)
+
+ @settings.command(name="hidden", aliases=["h"])
+ @PollsPermission.write.check
+ @docs(t.commands.poll.settings.hidden)
+ async def hidden(self, ctx: Context, status: bool):
+ if status:
+ msg: str = t.hidden.hidden
+ else:
+ msg: str = t.hidden.not_hidden
+
+ await PollsDefaultSettings.hidden.set(status)
+ await add_reactions(ctx.message, "white_check_mark")
+ await send_to_changelog(ctx.guild, msg)
+
@commands.command(usage=t.poll_usage, aliases=["teamvote", "tp"])
@PollsPermission.team_poll.check
@guild_only()
diff --git a/general/polls/models.py b/general/polls/models.py
index baae05700..138263d80 100644
--- a/general/polls/models.py
+++ b/general/polls/models.py
@@ -57,6 +57,15 @@ class Polls(Base):
keep: Union[Column, bool] = Column(Boolean)
+class Voted(Base):
+ __tablename__ = "voted_user"
+
+ id: Union[Column, int] = Column(BigInteger, primary_key=True, autoincrement=True, unique=True)
+ user_id: Union[Column, int] = Column(BigInteger)
+ option_id: Options = relationship("Options")
+ vote_weight: Union[Column, float] = Column(Float)
+
+
class Options(Base):
__tablename__ = "poll_options"
@@ -96,12 +105,3 @@ async def remove(self) -> None:
@staticmethod
async def get() -> list[RolesWeights]:
return await db.all(select(RolesWeights))
-
-
-class Voted(Base):
- __tablename__ = "voted_user"
-
- id: Union[Column, int] = Column(BigInteger, primary_key=True, autoincrement=True, unique=True)
- user_id: Union[Column, int] = Column(BigInteger)
- option_id: Options = relationship("Options")
- vote_weight: Union[Column, float] = Column(Float)
diff --git a/general/polls/translations/en.yml b/general/polls/translations/en.yml
index 2d81f3459..f32ae733f 100644
--- a/general/polls/translations/en.yml
+++ b/general/polls/translations/en.yml
@@ -1,8 +1,14 @@
commands:
- poll: poll commands
- quick: small poll with default options
- new: advanced poll with more options
- settings: poll settings
+ poll:
+ poll: poll commands
+ quick: small poll with default options
+ new: advanced poll with more options
+ settings:
+ settings: poll settings
+ roles_weights: manage weight for certain roles
+ duration: set the default hours a poll should be open
+ votes: set the default amount of votes a user can have on polls
+ hidden: set hide attribute for votes on polls
permissions:
team_poll: start a team poll
@@ -10,6 +16,10 @@ permissions:
write: edit poll configuration
delete: delete polls
+error:
+ weight_too_small: "Weight cant be lower than `0.1`"
+ cant_set_weight: Can't set weight!
+
poll_config:
title: Default poll configuration
duration:
@@ -28,7 +38,28 @@ poll_config:
name: "**Hidden Votes**"
roles:
name: "**Role Weights**"
- row: "\n{} -> `{}x`"
+ ev_row: "{} -> `{}x`"
+ row: "\n<@&{}> -> `{}x`"
+
+role_weight:
+ set: "Set vote weight for <@&{}> to `{}`"
+ reset: "Vote weight was reset for <@&{}>"
+
+duration:
+ set:
+ one: "Set default duration for poll to {} hour"
+ many: "Set default duration for poll to {} hours"
+ reset: "Set the default duration for polls to unlimited"
+
+votes:
+ set:
+ one: "Set default votes for a poll to {} vote"
+ many: "Set default votes for a poll to {} votes"
+ reset: "Set the default votes for polls to unlimited"
+
+hidden:
+ hidden: made votes hidden
+ not_hidden: made votes visible
poll: Poll
team_poll: Team Poll
From bf785b57f0d7e9308f62db5baa16365e6f338a92 Mon Sep 17 00:00:00 2001
From: NekoFanatic <83883849+NekoFanatic@users.noreply.github.com>
Date: Mon, 11 Apr 2022 17:29:13 +0200
Subject: [PATCH 03/44] removed notes
---
general/polls/models.py | 8 --------
1 file changed, 8 deletions(-)
diff --git a/general/polls/models.py b/general/polls/models.py
index 138263d80..9c4f57b31 100644
--- a/general/polls/models.py
+++ b/general/polls/models.py
@@ -10,14 +10,6 @@
from PyDrocsid.database import Base, UTCDateTime, db, select
-# tabelle für vote stimmen (ForeignKey)
-# konfigutierbar ausschluss der mute-rolle
-# userpolls abspecken
-# default werte in settings
-# wizzard weg (alles eine zeile)
-# yn so lassen
-
-
class Poll:
def __init__(self, owner: int, channel: int):
self.owner: int = owner
From c7b68da9ec604a49b7c5ca8117aa3365f1832f34 Mon Sep 17 00:00:00 2001
From: NekoFanatic <83883849+NekoFanatic@users.noreply.github.com>
Date: Mon, 11 Apr 2022 21:35:35 +0200
Subject: [PATCH 04/44] some changes
---
general/polls/cog.py | 23 +++++++++--
general/polls/models.py | 63 +++++++++++--------------------
general/polls/permissions.py | 1 +
general/polls/settings.py | 1 +
general/polls/translations/en.yml | 8 ++++
5 files changed, 52 insertions(+), 44 deletions(-)
diff --git a/general/polls/cog.py b/general/polls/cog.py
index cee791df7..3607a2c30 100644
--- a/general/polls/cog.py
+++ b/general/polls/cog.py
@@ -18,7 +18,7 @@
from PyDrocsid.util import check_wastebasket, is_teamler
from .colors import Colors
-from .models import RolesWeights
+from .models import RoleWeight
from .permissions import PollsPermission
from .settings import PollsDefaultSettings
from ...contributor import Contributor
@@ -203,7 +203,9 @@ async def settings(self, ctx: Context):
)
hide: bool = await PollsDefaultSettings.hidden.get()
embed.add_field(name=t.poll_config.hidden.name, value=str(hide), inline=False)
- roles = await RolesWeights.get()
+ anonymous: bool = await PollsDefaultSettings.anonymous.get()
+ embed.add_field(name=t.poll_config.anonymous.name, value=str(anonymous), inline=False)
+ roles = await RoleWeight.get()
everyone: int = await PollsDefaultSettings.everyone_power.get()
base: str = t.poll_config.roles.ev_row(ctx.guild.default_role, everyone)
if roles:
@@ -216,7 +218,7 @@ async def settings(self, ctx: Context):
@PollsPermission.write.check
@docs(t.commands.poll.settings.roles_weights)
async def roles_weights(self, ctx: Context, role: Role, weight: float = None):
- element = await db.get(RolesWeights, role_id=role.id)
+ element = await db.get(RoleWeight, role_id=role.id)
if not weight and not element:
raise CommandError(t.error.cant_set_weight)
@@ -228,7 +230,7 @@ async def roles_weights(self, ctx: Context, role: Role, weight: float = None):
element.weight = weight
msg: str = t.role_weight.set(role.id, weight)
elif weight and not element:
- await RolesWeights.create(role.id, weight)
+ await RoleWeight.create(role.id, weight)
msg: str = t.role_weight.set(role.id, weight)
else:
await element.remove()
@@ -278,6 +280,19 @@ async def hidden(self, ctx: Context, status: bool):
await add_reactions(ctx.message, "white_check_mark")
await send_to_changelog(ctx.guild, msg)
+ @settings.command(name="anonymous", aliases=["a"])
+ @PollsPermission.write.check
+ @docs(t.commands.poll.settings.anonymous)
+ async def anonymous(self, ctx: Context, status: bool):
+ if status:
+ msg: str = t.anonymous.is_on
+ else:
+ msg: str = t.anonymous.is_off
+
+ await PollsDefaultSettings.anonymous.set(status)
+ await add_reactions(ctx.message, "white_check_mark")
+ await send_to_changelog(ctx.guild, msg)
+
@commands.command(usage=t.poll_usage, aliases=["teamvote", "tp"])
@PollsPermission.team_poll.check
@guild_only()
diff --git a/general/polls/models.py b/general/polls/models.py
index 9c4f57b31..c43cc286c 100644
--- a/general/polls/models.py
+++ b/general/polls/models.py
@@ -1,7 +1,7 @@
from __future__ import annotations
from datetime import datetime
-from typing import Optional, Union
+from typing import Union
from discord.utils import utcnow
from sqlalchemy import BigInteger, Boolean, Column, Float, ForeignKey, Text
@@ -10,30 +10,12 @@
from PyDrocsid.database import Base, UTCDateTime, db, select
-class Poll:
- def __init__(self, owner: int, channel: int):
- self.owner: int = owner
- self.channel: int = channel
- self.message_id: int = 0
-
- self.question: str = ""
- self.type: str = "standard" # standard, team
- self.options: list[tuple[str, str]] = [] # [(emote, option), ...]
- self.max_votes: int = 1
- self.voted: dict[str, tuple[list[int], int]] = {} # {"user1": ([option_number, ...], weight), ...}
- self.votes: dict[str, int] # {option: number_of_votes, ...}
- self.roles: dict[str, float] = {} # {"role_id": weight, ...}
- self.hidden: bool = False
- self.duration: Optional[datetime] = None
- self.active: bool = False
-
-
-class Polls(Base):
- __tablename__ = "polls"
+class Poll(Base):
+ __tablename__ = "poll"
id: Union[Column, int] = Column(BigInteger, primary_key=True, autoincrement=True, unique=True)
- options: list[Options] = relationship("Options", back_populates="poll")
+ options: list[Option] = relationship("Option", back_populates="poll", cascade="all, delete")
message_id: Union[Column, int] = Column(BigInteger, unique=True)
poll_channel: Union[Column, int] = Column(BigInteger)
@@ -42,7 +24,7 @@ class Polls(Base):
title: Union[Column, str] = Column(Text(256))
poll_type: Union[Column, str] = Column(Text(50))
end_time: Union[Column, datetime] = Column(UTCDateTime)
- hidden_votes: Union[Column, bool] = Column(Boolean)
+ anonymous: Union[Column, bool] = Column(Boolean)
votes_amount: Union[Column, int] = Column(BigInteger)
poll_open: Union[Column, bool] = Column(Boolean)
can_delete: Union[Column, bool] = Column(Boolean)
@@ -54,30 +36,31 @@ class Voted(Base):
id: Union[Column, int] = Column(BigInteger, primary_key=True, autoincrement=True, unique=True)
user_id: Union[Column, int] = Column(BigInteger)
- option_id: Options = relationship("Options")
+ option_id: Union[Column, int] = Column(BigInteger, ForeignKey("poll_option.id"))
+ option: Option = relationship("Option", back_populates="votes", cascade="all, delete")
vote_weight: Union[Column, float] = Column(Float)
-class Options(Base):
- __tablename__ = "poll_options"
+class Option(Base):
+ __tablename__ = "poll_option"
- id: Union[Column, int] = Column(
- BigInteger, ForeignKey("voted_user.option_id"), primary_key=True, autoincrement=True, unique=True
- )
- poll_id: Union[Column, int] = Column(BigInteger, ForeignKey("polls.id"))
+ id: Union[Column, int] = Column(BigInteger, primary_key=True, autoincrement=True, unique=True)
+ poll_id: Union[Column, int] = Column(BigInteger, ForeignKey("poll.id"))
+ votes: list[Voted] = relationship("Voted", back_populates="option")
+ poll: Poll = relationship("Poll", back_populates="options")
emote: Union[Column, str] = Column(Text(30))
option: Union[Column, str] = Column(Text(150))
@staticmethod
- async def create(poll: int, emote: str, option_text: str) -> Options:
- options = Options(poll_id=poll, emote=emote, option=option_text)
+ async def create(poll: int, emote: str, option_text: str) -> Option:
+ options = Option(poll_id=poll, emote=emote, option=option_text)
await db.add(options)
return options
-class RolesWeights(Base):
- __tablename__ = "roles_weight"
+class RoleWeight(Base):
+ __tablename__ = "role_weight"
id: Union[Column, int] = Column(BigInteger, primary_key=True, autoincrement=True, unique=True)
role_id: Union[Column, int] = Column(BigInteger, unique=True)
@@ -85,15 +68,15 @@ class RolesWeights(Base):
timestamp: Union[Column, datetime] = Column(UTCDateTime)
@staticmethod
- async def create(role: int, weight: float) -> RolesWeights:
- roles_weights = RolesWeights(role_id=role, weight=weight, timestamp=utcnow())
- await db.add(roles_weights)
+ async def create(role: int, weight: float) -> RoleWeight:
+ role_weight = RoleWeight(role_id=role, weight=weight, timestamp=utcnow())
+ await db.add(role_weight)
- return roles_weights
+ return role_weight
async def remove(self) -> None:
await db.delete(self)
@staticmethod
- async def get() -> list[RolesWeights]:
- return await db.all(select(RolesWeights))
+ async def get() -> list[RoleWeight]:
+ return await db.all(select(RoleWeight))
diff --git a/general/polls/permissions.py b/general/polls/permissions.py
index 6b5384d48..7d4b6992e 100644
--- a/general/polls/permissions.py
+++ b/general/polls/permissions.py
@@ -13,3 +13,4 @@ def description(self) -> str:
read = auto()
write = auto()
delete = auto()
+ anonymous_bypass = auto()
diff --git a/general/polls/settings.py b/general/polls/settings.py
index 69f881d01..6937bf6dd 100644
--- a/general/polls/settings.py
+++ b/general/polls/settings.py
@@ -7,3 +7,4 @@ class PollsDefaultSettings(Settings):
type = "standard"
hidden = False
everyone_power = 1.0
+ anonymous = False
diff --git a/general/polls/translations/en.yml b/general/polls/translations/en.yml
index f32ae733f..e685eb911 100644
--- a/general/polls/translations/en.yml
+++ b/general/polls/translations/en.yml
@@ -9,12 +9,14 @@ commands:
duration: set the default hours a poll should be open
votes: set the default amount of votes a user can have on polls
hidden: set hide attribute for votes on polls
+ anonymous: set if user can see who voted on a poll
permissions:
team_poll: start a team poll
read: read poll configuration
write: edit poll configuration
delete: delete polls
+ anonymous_bypass: can see user, even if poll is anonymous
error:
weight_too_small: "Weight cant be lower than `0.1`"
@@ -36,6 +38,8 @@ poll_config:
unlimited: unlimited
hidden:
name: "**Hidden Votes**"
+ anonymous:
+ name: "Anonymous"
roles:
name: "**Role Weights**"
ev_row: "{} -> `{}x`"
@@ -61,6 +65,10 @@ hidden:
hidden: made votes hidden
not_hidden: made votes visible
+anonymous:
+ is_on: made default poll votes anonymous
+ is_off: made default poll votes visible
+
poll: Poll
team_poll: Team Poll
vote_explanation: Vote using the reactions below!
From 81c52f99843139fdfa11de2903c3559a6ebe781c Mon Sep 17 00:00:00 2001
From: NekoFanatic <83883849+NekoFanatic@users.noreply.github.com>
Date: Mon, 11 Apr 2022 23:58:50 +0200
Subject: [PATCH 05/44] First version of select menus
---
general/polls/cog.py | 55 ++++++++++++++++++++++---------
general/polls/translations/en.yml | 7 ++++
2 files changed, 47 insertions(+), 15 deletions(-)
diff --git a/general/polls/cog.py b/general/polls/cog.py
index 3607a2c30..ed24a9116 100644
--- a/general/polls/cog.py
+++ b/general/polls/cog.py
@@ -2,9 +2,10 @@
import string
from typing import Optional, Tuple
-from discord import Embed, Forbidden, Guild, Member, Message, PartialEmoji, Role
+from discord import Embed, Forbidden, Guild, Member, Message, PartialEmoji, Role, SelectOption
from discord.ext import commands
from discord.ext.commands import CommandError, Context, UserInputError, guild_only
+from discord.ui import Select, View
from discord.utils import utcnow
from PyDrocsid.cog import Cog
@@ -28,7 +29,7 @@
tg = t.g
t = t.polls
-MAX_OPTIONS = 20 # Discord reactions limit
+MAX_OPTIONS = 25 # Discord select menu limit
default_emojis = [name_to_emoji[f"regional_indicator_{x}"] for x in string.ascii_lowercase]
@@ -42,16 +43,22 @@ async def get_teampoll_embed(message: Message) -> Tuple[Optional[Embed], Optiona
async def send_poll(
- ctx: Context, title: str, args: str, field: Optional[Tuple[str, str]] = None, allow_delete: bool = True
+ ctx: Context,
+ title: str,
+ args: str,
+ max_choices: int = None,
+ field: Optional[Tuple[str, str]] = None,
+ allow_delete: bool = True,
):
question, *options = [line.replace("\x00", "\n") for line in args.replace("\\\n", "\x00").split("\n") if line]
if not options:
raise CommandError(t.missing_options)
- if len(options) > MAX_OPTIONS - allow_delete:
- raise CommandError(t.too_many_options(MAX_OPTIONS - allow_delete))
+ if len(options) > MAX_OPTIONS:
+ raise CommandError(t.too_many_options(MAX_OPTIONS))
options = [PollOption(ctx, line, i) for i, line in enumerate(options)]
+ print([option.__dict__ for option in options])
if any(len(str(option)) > EmbedLimits.FIELD_VALUE for option in options):
raise CommandError(t.option_too_long(EmbedLimits.FIELD_VALUE))
@@ -70,15 +77,25 @@ async def send_poll(
if field:
embed.add_field(name=field[0], value=field[1], inline=False)
- poll: Message = await ctx.send(embed=embed)
+ if not max_choices:
+ place = t.select.place
+ max_value = len(options)
+ else:
+ place: str = t.select.placeholder(max_choices)
+ max_value = max_choices if not len(options) > max_choices else len(options)
- try:
- for option in options:
- await poll.add_reaction(option.emoji)
- if allow_delete:
- await poll.add_reaction(name_to_emoji["wastebasket"])
- except Forbidden:
- raise CommandError(t.could_not_add_reactions(ctx.channel.mention))
+ select = Select(
+ placeholder=place,
+ max_values=max_value,
+ options=[
+ SelectOption(label=t.select.label(index + 1), emoji=option.emoji, description=option.option)
+ for index, option in enumerate(options)
+ ],
+ )
+
+ view = View()
+ view.add_item(select)
+ await ctx.send(embed=embed, view=view)
class PollsCog(Cog, name="Polls"):
@@ -177,7 +194,7 @@ async def poll(self, ctx: Context):
@docs(t.commands.poll.quick)
async def quick(self, ctx: Context, *, args: str):
- await send_poll(ctx, t.poll, args)
+ await send_poll(ctx, t.poll, args, await PollsDefaultSettings.max_choices.get())
@poll.group(name="settings", aliases=["s"])
@PollsPermission.read.check
@@ -263,6 +280,9 @@ async def votes(self, ctx: Context, votes: int = None):
else:
msg: str = t.votes.set(votes)
+ if not 0 < votes < 25:
+ votes = 0
+
await PollsDefaultSettings.max_choices.set(votes)
await add_reactions(ctx.message, "white_check_mark")
await send_to_changelog(ctx.guild, msg)
@@ -303,7 +323,12 @@ async def teampoll(self, ctx: Context, *, args: str):
"""
await send_poll(
- ctx, t.team_poll, args, field=(tg.status, await self.get_reacted_teamlers()), allow_delete=False
+ ctx,
+ t.team_poll,
+ args,
+ await PollsDefaultSettings.max_choices.get(),
+ field=(tg.status, await self.get_reacted_teamlers()),
+ allow_delete=False,
)
@commands.command(aliases=["yn"])
diff --git a/general/polls/translations/en.yml b/general/polls/translations/en.yml
index e685eb911..6efea2923 100644
--- a/general/polls/translations/en.yml
+++ b/general/polls/translations/en.yml
@@ -69,6 +69,13 @@ anonymous:
is_on: made default poll votes anonymous
is_off: made default poll votes visible
+select:
+ place: Select Options
+ placeholder:
+ one: "Select up to {} option!"
+ more: "Select up to {} options!"
+ label: "Option {}."
+
poll: Poll
team_poll: Team Poll
vote_explanation: Vote using the reactions below!
From e40e85c07acce7bfd9016ec700ed7c5170b044c4 Mon Sep 17 00:00:00 2001
From: NekoFanatic <83883849+NekoFanatic@users.noreply.github.com>
Date: Tue, 12 Apr 2022 14:17:27 +0200
Subject: [PATCH 06/44] Added setting-command for everyone-role
---
general/polls/cog.py | 17 +++++++++++++++++
general/polls/translations/en.yml | 7 ++++++-
2 files changed, 23 insertions(+), 1 deletion(-)
diff --git a/general/polls/cog.py b/general/polls/cog.py
index ed24a9116..f08b5c898 100644
--- a/general/polls/cog.py
+++ b/general/polls/cog.py
@@ -313,6 +313,23 @@ async def anonymous(self, ctx: Context, status: bool):
await add_reactions(ctx.message, "white_check_mark")
await send_to_changelog(ctx.guild, msg)
+ @settings.command(name="everyone", aliases=["e"])
+ @PollsPermission.write.check
+ @docs(t.commands.poll.settings.everyone)
+ async def everyone(self, ctx: Context, weight: float = None):
+ if weight and weight < 0.1:
+ raise CommandError(t.error.weight_too_small)
+
+ if not weight:
+ await PollsDefaultSettings.everyone_power.set(1.0)
+ msg: str = t.weight_everyone.reset
+ else:
+ await PollsDefaultSettings.everyone_power.set(weight)
+ msg: str = t.weight_everyone.set(weight)
+
+ await add_reactions(ctx.message, "white_check_mark")
+ await send_to_changelog(ctx.guild, msg)
+
@commands.command(usage=t.poll_usage, aliases=["teamvote", "tp"])
@PollsPermission.team_poll.check
@guild_only()
diff --git a/general/polls/translations/en.yml b/general/polls/translations/en.yml
index 6efea2923..29e633784 100644
--- a/general/polls/translations/en.yml
+++ b/general/polls/translations/en.yml
@@ -10,6 +10,7 @@ commands:
votes: set the default amount of votes a user can have on polls
hidden: set hide attribute for votes on polls
anonymous: set if user can see who voted on a poll
+ everyone: manage role weight for the default role
permissions:
team_poll: start a team poll
@@ -47,7 +48,11 @@ poll_config:
role_weight:
set: "Set vote weight for <@&{}> to `{}`"
- reset: "Vote weight was reset for <@&{}>"
+ reset: "Vote weight has been reset for <@&{}>"
+
+weight_everyone:
+ set: "Set vote weight for the default role to `{}`"
+ reset: Vote weight for the default role has been reset
duration:
set:
From 0f962920ca9a5e22b3f58abe30a825d0680f3340 Mon Sep 17 00:00:00 2001
From: NekoFanatic <83883849+NekoFanatic@users.noreply.github.com>
Date: Tue, 12 Apr 2022 14:42:16 +0200
Subject: [PATCH 07/44] fixed mistakes
---
general/polls/cog.py | 3 +--
general/polls/translations/en.yml | 2 +-
2 files changed, 2 insertions(+), 3 deletions(-)
diff --git a/general/polls/cog.py b/general/polls/cog.py
index f08b5c898..c04532ffd 100644
--- a/general/polls/cog.py
+++ b/general/polls/cog.py
@@ -58,7 +58,6 @@ async def send_poll(
raise CommandError(t.too_many_options(MAX_OPTIONS))
options = [PollOption(ctx, line, i) for i, line in enumerate(options)]
- print([option.__dict__ for option in options])
if any(len(str(option)) > EmbedLimits.FIELD_VALUE for option in options):
raise CommandError(t.option_too_long(EmbedLimits.FIELD_VALUE))
@@ -82,7 +81,7 @@ async def send_poll(
max_value = len(options)
else:
place: str = t.select.placeholder(max_choices)
- max_value = max_choices if not len(options) > max_choices else len(options)
+ max_value = len(options) if max_choices >= len(options) else max_choices
select = Select(
placeholder=place,
diff --git a/general/polls/translations/en.yml b/general/polls/translations/en.yml
index 29e633784..e17041b98 100644
--- a/general/polls/translations/en.yml
+++ b/general/polls/translations/en.yml
@@ -78,7 +78,7 @@ select:
place: Select Options
placeholder:
one: "Select up to {} option!"
- more: "Select up to {} options!"
+ many: "Select up to {} options!"
label: "Option {}."
poll: Poll
From 2acbb65d5b823b48b1444e3396f29481cad0f26f Mon Sep 17 00:00:00 2001
From: NekoFanatic <83883849+NekoFanatic@users.noreply.github.com>
Date: Tue, 12 Apr 2022 20:19:21 +0200
Subject: [PATCH 08/44] saving commit
---
general/polls/cog.py | 176 +++++++++++++-----------------
general/polls/translations/en.yml | 33 ++++--
2 files changed, 99 insertions(+), 110 deletions(-)
diff --git a/general/polls/cog.py b/general/polls/cog.py
index c04532ffd..d1ed9d54e 100644
--- a/general/polls/cog.py
+++ b/general/polls/cog.py
@@ -1,8 +1,10 @@
import re
import string
-from typing import Optional, Tuple
+from argparse import ArgumentParser
+from datetime import datetime
+from typing import Optional, Tuple, Union
-from discord import Embed, Forbidden, Guild, Member, Message, PartialEmoji, Role, SelectOption
+from discord import Embed, Forbidden, Guild, Member, Message, Role, SelectOption
from discord.ext import commands
from discord.ext.commands import CommandError, Context, UserInputError, guild_only
from discord.ui import Select, View
@@ -13,10 +15,11 @@
from PyDrocsid.database import db
from PyDrocsid.embeds import EmbedLimits, send_long_embed
from PyDrocsid.emojis import emoji_to_name, name_to_emoji
-from PyDrocsid.events import StopEventHandling
+
+# from PyDrocsid.redis import redis
from PyDrocsid.settings import RoleSettings
from PyDrocsid.translations import t
-from PyDrocsid.util import check_wastebasket, is_teamler
+from PyDrocsid.util import is_teamler
from .colors import Colors
from .models import RoleWeight
@@ -34,6 +37,25 @@
default_emojis = [name_to_emoji[f"regional_indicator_{x}"] for x in string.ascii_lowercase]
+def create_select_view(select: Select) -> View:
+ view = View()
+ view.add_item(select)
+
+ return view
+
+
+async def get_parser() -> ArgumentParser:
+ parser = ArgumentParser()
+ parser.add_argument("--type", "-T", default="standard", choices=["standard", "team"], type=str)
+ parser.add_argument(
+ "--deadline", "-D", default=await PollsDefaultSettings.duration.get(), type=Union[int, datetime]
+ )
+ parser.add_argument("--anonymous", "-A", default=await PollsDefaultSettings.anonymous.get(), type=bool)
+ parser.add_argument("--choices", "-C", default=await PollsDefaultSettings.max_choices.get(), type=int)
+
+ return parser
+
+
async def get_teampoll_embed(message: Message) -> Tuple[Optional[Embed], Optional[int]]:
for embed in message.embeds:
for i, field in enumerate(embed.fields):
@@ -80,10 +102,11 @@ async def send_poll(
place = t.select.place
max_value = len(options)
else:
- place: str = t.select.placeholder(max_choices)
- max_value = len(options) if max_choices >= len(options) else max_choices
+ use = len(options) if max_choices >= len(options) else max_choices
+ place: str = t.select.placeholder(cnt=use)
+ max_value = use
- select = Select(
+ select = MySelect(
placeholder=place,
max_values=max_value,
options=[
@@ -92,9 +115,23 @@ async def send_poll(
],
)
- view = View()
- view.add_item(select)
- await ctx.send(embed=embed, view=view)
+ await ctx.send(embed=embed, view=create_select_view(select))
+
+
+async def edit_team_embed(embed: Embed, user: int, option: str):
+ pass
+
+
+class MySelect(Select):
+ async def callback(self, interaction):
+ message: Message = interaction.message
+
+ if await get_teampoll_embed(message) != (None, None):
+ pass
+ else:
+ pass
+ print(self.values, interaction.user.id)
+ return interaction.user.id, self.values
class PollsCog(Cog, name="Polls"):
@@ -134,54 +171,6 @@ async def get_reacted_teamlers(self, message: Optional[Message] = None) -> str:
teamlers: list[str]
return t.teamlers_missing(teamlers=", ".join(teamlers), last=last, cnt=len(teamlers) + 1)
- async def on_raw_reaction_add(self, message: Message, emoji: PartialEmoji, member: Member):
- if member.bot or message.guild is None:
- return
-
- if await check_wastebasket(message, member, emoji, t.created_by, PollsPermission.delete):
- await message.delete()
- raise StopEventHandling
-
- embed, index = await get_teampoll_embed(message)
- if embed is None:
- return
-
- if not await is_teamler(member):
- try:
- await message.remove_reaction(emoji, member)
- except Forbidden:
- pass
- raise StopEventHandling
-
- for reaction in message.reactions:
- if reaction.emoji == emoji.name:
- break
- else:
- return
-
- if not reaction.me:
- return
-
- value = await self.get_reacted_teamlers(message)
- embed.set_field_at(index, name=tg.status, value=value, inline=False)
- await message.edit(embed=embed)
-
- async def on_raw_reaction_remove(self, message: Message, _, member: Member):
- if member.bot or message.guild is None:
- return
- embed, index = await get_teampoll_embed(message)
- if embed is not None:
- user_reacted = False
- for reaction in message.reactions:
- if reaction.me and member in await reaction.users().flatten():
- user_reacted = True
- break
- if not user_reacted and await is_teamler(member):
- value = await self.get_reacted_teamlers(message)
- embed.set_field_at(index, name=tg.status, value=value, inline=False)
- await message.edit(embed=embed)
- return
-
@commands.group(name="poll", aliases=["vote"])
@guild_only()
@docs(t.commands.poll.poll)
@@ -189,12 +178,6 @@ async def poll(self, ctx: Context):
if not ctx.subcommand_passed:
raise UserInputError
- @poll.command(name="quick", usage=t.poll_usage, aliases=["q"])
- @docs(t.commands.poll.quick)
- async def quick(self, ctx: Context, *, args: str):
-
- await send_poll(ctx, t.poll, args, await PollsDefaultSettings.max_choices.get())
-
@poll.group(name="settings", aliases=["s"])
@PollsPermission.read.check
@docs(t.commands.poll.settings.settings)
@@ -208,13 +191,13 @@ async def settings(self, ctx: Context):
time: int = await PollsDefaultSettings.duration.get()
embed.add_field(
name=t.poll_config.duration.name,
- value=t.poll_config.duration.time(time) if not time <= 0 else t.poll_config.duration.unlimited,
+ value=t.poll_config.duration.time(cnt=time) if not time <= 0 else t.poll_config.duration.unlimited,
inline=False,
)
choice: int = await PollsDefaultSettings.max_choices.get()
embed.add_field(
name=t.poll_config.choices.name,
- value=t.poll_config.choices.amount(choice) if not choice <= 0 else t.poll_config.choices.unlimited,
+ value=t.poll_config.choices.amount(cnt=choice) if not choice <= 0 else t.poll_config.choices.unlimited,
inline=False,
)
hide: bool = await PollsDefaultSettings.hidden.get()
@@ -263,7 +246,7 @@ async def duration(self, ctx: Context, hours: int = None):
hours = 0
msg: str = t.duration.reset()
else:
- msg: str = t.duration.set(hours)
+ msg: str = t.duration.set(cnt=hours)
await PollsDefaultSettings.duration.set(hours)
await add_reactions(ctx.message, "white_check_mark")
@@ -277,7 +260,7 @@ async def votes(self, ctx: Context, votes: int = None):
votes = 0
msg: str = t.votes.reset
else:
- msg: str = t.votes.set(votes)
+ msg: str = t.votes.set(cnt=votes)
if not 0 < votes < 25:
votes = 0
@@ -324,36 +307,29 @@ async def everyone(self, ctx: Context, weight: float = None):
msg: str = t.weight_everyone.reset
else:
await PollsDefaultSettings.everyone_power.set(weight)
- msg: str = t.weight_everyone.set(weight)
+ msg: str = t.weight_everyone.set(cnt=weight)
await add_reactions(ctx.message, "white_check_mark")
await send_to_changelog(ctx.guild, msg)
- @commands.command(usage=t.poll_usage, aliases=["teamvote", "tp"])
- @PollsPermission.team_poll.check
- @guild_only()
- async def teampoll(self, ctx: Context, *, args: str):
- """
- Starts a poll and shows, which teamler has not voted yet.
- Multiline options can be specified using a `\\` at the end of a line
- """
-
- await send_poll(
- ctx,
- t.team_poll,
- args,
- await PollsDefaultSettings.max_choices.get(),
- field=(tg.status, await self.get_reacted_teamlers()),
- allow_delete=False,
- )
+ @poll.command(name="quick", usage=t.poll_usage, aliases=["q"])
+ @docs(t.commands.poll.quick)
+ async def quick(self, ctx: Context, *, args: str):
+
+ await send_poll(ctx, t.poll, args, await PollsDefaultSettings.max_choices.get())
+
+ @poll.command(name="new", usage=t.usage.new)
+ @docs(t.commands.poll.new)
+ async def new(self, ctx: Context, *, args: str = None):
+ parser = await get_parser()
+ parsed = parser.parse_known_args(args)
+
+ print(parsed)
@commands.command(aliases=["yn"])
@guild_only()
+ @docs(t.commands.yes_no)
async def yesno(self, ctx: Context, message: Optional[Message] = None, text: Optional[str] = None):
- """
- adds thumbsup and thumbsdown reactions to the message
- """
-
if message is None or message.guild is None or text:
message = ctx.message
@@ -375,22 +351,22 @@ async def yesno(self, ctx: Context, message: Optional[Message] = None, text: Opt
@commands.command(aliases=["tyn"])
@PollsPermission.team_poll.check
@guild_only()
+ @docs(t.commands.team_yes_no)
async def team_yesno(self, ctx: Context, *, text: str):
- """
- Starts a yes/no poll and shows, which teamler has not voted yet.
- """
-
+ ops = [(t.yes_no.in_favor, "thumbsup"), (t.yes_no.against, "thumbsdown"), (t.yes_no.abstention, "zzz")]
+ select = MySelect(
+ placeholder=t.select.placeholder(cnt=1),
+ options=[SelectOption(label=op[0], emoji=name_to_emoji[op[1]]) for op in ops],
+ )
embed = Embed(title=t.team_poll, description=text, color=Colors.Polls, timestamp=utcnow())
embed.set_author(name=str(ctx.author), icon_url=ctx.author.display_avatar.url)
+ embed.add_field(name=t.yes_no.in_favor, value=t.yes_no.count(0, cnt=0), inline=True)
+ embed.add_field(name=t.yes_no.against, value=t.yes_no.count(0, cnt=0), inline=True)
+ embed.add_field(name=t.yes_no.abstention, value=t.yes_no.count(0, cnt=0), inline=True)
embed.add_field(name=tg.status, value=await self.get_reacted_teamlers(), inline=False)
- message: Message = await ctx.send(embed=embed)
- try:
- await message.add_reaction(name_to_emoji["+1"])
- await message.add_reaction(name_to_emoji["-1"])
- except Forbidden:
- raise CommandError(t.could_not_add_reactions(message.channel.mention))
+ await ctx.send(embed=embed, view=create_select_view(select))
class PollOption:
diff --git a/general/polls/translations/en.yml b/general/polls/translations/en.yml
index e17041b98..2d0fca844 100644
--- a/general/polls/translations/en.yml
+++ b/general/polls/translations/en.yml
@@ -11,6 +11,8 @@ commands:
hidden: set hide attribute for votes on polls
anonymous: set if user can see who voted on a poll
everyone: manage role weight for the default role
+ yes_no: add thumbs-up/down emotes on a message
+ team_yes_no: starts a yes/no poll and shows, which teamler has not voted yet.
permissions:
team_poll: start a team poll
@@ -28,14 +30,14 @@ poll_config:
duration:
name: "**Duration**"
time:
- one: "{} hour"
- many: "{} hours"
+ one: "{cnt} hour"
+ many: "{cnt} hours"
unlimited: unlimited
choices:
name: "**Choices per user**"
amount:
- one: "{} choice per user"
- many: "{} choices per user"
+ one: "{cnt} choice per user"
+ many: "{cnt} choices per user"
unlimited: unlimited
hidden:
name: "**Hidden Votes**"
@@ -56,14 +58,14 @@ weight_everyone:
duration:
set:
- one: "Set default duration for poll to {} hour"
- many: "Set default duration for poll to {} hours"
+ one: "Set default duration for poll to {cnt} hour"
+ many: "Set default duration for poll to {cnt} hours"
reset: "Set the default duration for polls to unlimited"
votes:
set:
- one: "Set default votes for a poll to {} vote"
- many: "Set default votes for a poll to {} votes"
+ one: "Set default votes for a poll to {cnt} vote"
+ many: "Set default votes for a poll to {cnt} votes"
reset: "Set the default votes for polls to unlimited"
hidden:
@@ -77,10 +79,21 @@ anonymous:
select:
place: Select Options
placeholder:
- one: "Select up to {} option!"
- many: "Select up to {} options!"
+ one: "Select an option!"
+ many: "Select up to {cnt} options!"
label: "Option {}."
+usage:
+ new: "[--type {standard,team}] [--deadline DEADLINE] [--anonymous ANONYMOUS] [--choices CHOICES]"
+
+yes_no:
+ in_favor: "Yes"
+ against: "No"
+ abstention: "Abstention"
+ count:
+ one: "{cnt} vote ({}%)"
+ many: "{cnt} votes ({}%)"
+
poll: Poll
team_poll: Team Poll
vote_explanation: Vote using the reactions below!
From 31c94ba0d50ed1569dfe612981f796046ab4974b Mon Sep 17 00:00:00 2001
From: NekoFanatic <83883849+NekoFanatic@users.noreply.github.com>
Date: Wed, 13 Apr 2022 01:16:07 +0200
Subject: [PATCH 09/44] some changes
---
general/polls/cog.py | 109 +++++++++++++++++++++++++-----
general/polls/models.py | 53 ++++++++++++---
general/polls/translations/en.yml | 1 +
3 files changed, 136 insertions(+), 27 deletions(-)
diff --git a/general/polls/cog.py b/general/polls/cog.py
index d1ed9d54e..fd393ae30 100644
--- a/general/polls/cog.py
+++ b/general/polls/cog.py
@@ -12,17 +12,15 @@
from PyDrocsid.cog import Cog
from PyDrocsid.command import add_reactions, docs
-from PyDrocsid.database import db
+from PyDrocsid.database import db, db_wrapper, filter_by
from PyDrocsid.embeds import EmbedLimits, send_long_embed
from PyDrocsid.emojis import emoji_to_name, name_to_emoji
-
-# from PyDrocsid.redis import redis
from PyDrocsid.settings import RoleSettings
from PyDrocsid.translations import t
from PyDrocsid.util import is_teamler
from .colors import Colors
-from .models import RoleWeight
+from .models import RoleWeight, TeamYesNo, YesNoUser
from .permissions import PollsPermission
from .settings import PollsDefaultSettings
from ...contributor import Contributor
@@ -44,6 +42,12 @@ def create_select_view(select: Select) -> View:
return view
+def get_percentage(values: list[float]) -> list[tuple[float, float]]:
+ together = sum(values)
+
+ return [(value, round(((value / together) * 100), 2)) for value in values]
+
+
async def get_parser() -> ArgumentParser:
parser = ArgumentParser()
parser.add_argument("--type", "-T", default="standard", choices=["standard", "team"], type=str)
@@ -56,12 +60,12 @@ async def get_parser() -> ArgumentParser:
return parser
-async def get_teampoll_embed(message: Message) -> Tuple[Optional[Embed], Optional[int]]:
+async def get_teampoll_embed(message: Message) -> Tuple[Optional[str], Optional[Embed], Optional[int]]:
for embed in message.embeds:
for i, field in enumerate(embed.fields):
if tg.status == field.name:
- return embed, i
- return None, None
+ return embed.title, embed, i
+ return None, None, None
async def send_poll(
@@ -118,20 +122,90 @@ async def send_poll(
await ctx.send(embed=embed, view=create_select_view(select))
-async def edit_team_embed(embed: Embed, user: int, option: str):
- pass
+async def edit_team_yn(embed: Embed, poll: TeamYesNo, missing: list[Member]) -> Embed:
+ calc = get_percentage([poll.in_favor, poll.against, poll.abstention])
+ for index, field in enumerate(embed.fields):
+ if field.name == t.yes_no.in_favor:
+ embed.set_field_at(index, name=field.name, value=t.yes_no.count(calc[0][1], cnt=calc[0][0]))
+ elif field.name == t.yes_no.against:
+ embed.set_field_at(index, name=field.name, value=t.yes_no.count(calc[1][1], cnt=calc[1][0]))
+ elif field.name == t.yes_no.abstention:
+ embed.set_field_at(index, name=field.name, value=t.yes_no.count(calc[2][1], cnt=calc[2][0]))
+ if field.name == tg.status:
+ missing.sort(key=lambda m: str(m).lower())
+ *teamlers, last = (x.mention for x in missing)
+ teamlers: list[str]
+ embed.set_field_at(
+ index,
+ name=field.name,
+ value=t.teamlers_missing(teamlers=", ".join(teamlers), last=last, cnt=len(teamlers) + 1),
+ )
+
+ return embed
+
+
+async def get_teamler(guild: Guild, team_roles: list[str]) -> set[Member]:
+ teamlers: set[Member] = set()
+ for role_name in team_roles:
+ if not (team_role := guild.get_role(await RoleSettings.get(role_name))):
+ continue
+
+ teamlers.update(member for member in team_role.members if not member.bot)
+
+ return teamlers
class MySelect(Select):
+ @db_wrapper
async def callback(self, interaction):
message: Message = interaction.message
+ teamlers: set[Member] = await get_teamler(interaction.guild, ["team"])
+ team_poll = await get_teampoll_embed(message)
+
+ if team_poll[0] == t.team_yn_poll:
+ if interaction.user not in teamlers:
+ return
+
+ poll = await db.get(TeamYesNo, message_id=message.id)
+
+ if not (user := await db.get(YesNoUser, poll_id=message.id)):
+ user = await YesNoUser.create(interaction.user.id, message.id, int(self.values[0]))
+ if int(self.values[0]) == 0:
+ poll.in_favor += 1
+ elif int(self.values[0]) == 1:
+ poll.against += 1
+ else:
+ poll.abstention += 1
+ else:
+ old_user_option = int(user.option)
+ user.option = int(self.values[0])
+ if int(self.values[0]) == 0:
+ poll.in_favor += 1
+ elif int(self.values[0]) == 1:
+ poll.against += 1
+ else:
+ poll.abstention += 1
+
+ if old_user_option == 0:
+ poll.in_favor -= 1
+ elif old_user_option == 1:
+ poll.against -= 1
+ else:
+ poll.abstention -= 1
+
+ rows = await db.all(filter_by(YesNoUser, poll_id=message.id))
+ user_ids = [user.user for user in rows]
+ missing: list[Member] = [team for team in teamlers if team.id not in user_ids]
+
+ embed = await edit_team_yn(team_poll[1], poll, missing)
+ await message.edit(embed=embed)
+
+ elif team_poll[0] == t.team_poll:
+ if interaction.user.id not in teamlers:
+ return
- if await get_teampoll_embed(message) != (None, None):
- pass
else:
pass
- print(self.values, interaction.user.id)
- return interaction.user.id, self.values
class PollsCog(Cog, name="Polls"):
@@ -353,12 +427,12 @@ async def yesno(self, ctx: Context, message: Optional[Message] = None, text: Opt
@guild_only()
@docs(t.commands.team_yes_no)
async def team_yesno(self, ctx: Context, *, text: str):
- ops = [(t.yes_no.in_favor, "thumbsup"), (t.yes_no.against, "thumbsdown"), (t.yes_no.abstention, "zzz")]
+ ops = [(t.yes_no.in_favor, "thumbsup", 0), (t.yes_no.against, "thumbsdown", 1), (t.yes_no.abstention, "zzz", 2)]
select = MySelect(
placeholder=t.select.placeholder(cnt=1),
- options=[SelectOption(label=op[0], emoji=name_to_emoji[op[1]]) for op in ops],
+ options=[SelectOption(label=op[0], emoji=name_to_emoji[op[1]], value=str(op[2])) for op in ops],
)
- embed = Embed(title=t.team_poll, description=text, color=Colors.Polls, timestamp=utcnow())
+ embed = Embed(title=t.team_yn_poll, description=text, color=Colors.Polls, timestamp=utcnow())
embed.set_author(name=str(ctx.author), icon_url=ctx.author.display_avatar.url)
embed.add_field(name=t.yes_no.in_favor, value=t.yes_no.count(0, cnt=0), inline=True)
@@ -366,7 +440,8 @@ async def team_yesno(self, ctx: Context, *, text: str):
embed.add_field(name=t.yes_no.abstention, value=t.yes_no.count(0, cnt=0), inline=True)
embed.add_field(name=tg.status, value=await self.get_reacted_teamlers(), inline=False)
- await ctx.send(embed=embed, view=create_select_view(select))
+ msg: Message = await ctx.send(embed=embed, view=create_select_view(select))
+ await TeamYesNo.create(msg.id)
class PollOption:
diff --git a/general/polls/models.py b/general/polls/models.py
index c43cc286c..aed3d515b 100644
--- a/general/polls/models.py
+++ b/general/polls/models.py
@@ -10,6 +10,39 @@
from PyDrocsid.database import Base, UTCDateTime, db, select
+class TeamYesNo(Base):
+ __tablename__ = "team_yes_no"
+
+ message_id: Union[Column, int] = Column(BigInteger, unique=True, primary_key=True)
+ users: list[YesNoUser] = relationship("YesNoUser", back_populates="poll", cascade="all, delete")
+ in_favor: Union[Column, int] = Column(Float)
+ against: Union[Column, int] = Column(Float)
+ abstention: Union[Column, int] = Column(Float)
+ timestamp: Union[Column, datetime] = Column(UTCDateTime)
+
+ @staticmethod
+ async def create(message_id: int) -> TeamYesNo:
+ row = TeamYesNo(message_id=message_id, in_favor=0, against=0, abstention=0, timestamp=utcnow())
+ await db.add(row)
+ return row
+
+
+class YesNoUser(Base):
+ __tablename__ = "team_yes_no_voter"
+
+ id: Union[Column, int] = Column(BigInteger, primary_key=True, autoincrement=True, unique=True)
+ poll_id: Union[Column, int] = Column(BigInteger, ForeignKey("team_yes_no.message_id"))
+ poll: TeamYesNo = relationship("TeamYesNo", back_populates="users")
+ option: Union[Column, int] = Column(BigInteger)
+ user: Union[Column, int] = Column(BigInteger)
+
+ @staticmethod
+ async def create(user: int, poll_id: int, option: int) -> YesNoUser:
+ row = YesNoUser(user=user, poll_id=poll_id, option=option)
+ await db.add(row)
+ return row
+
+
class Poll(Base):
__tablename__ = "poll"
@@ -31,16 +64,6 @@ class Poll(Base):
keep: Union[Column, bool] = Column(Boolean)
-class Voted(Base):
- __tablename__ = "voted_user"
-
- id: Union[Column, int] = Column(BigInteger, primary_key=True, autoincrement=True, unique=True)
- user_id: Union[Column, int] = Column(BigInteger)
- option_id: Union[Column, int] = Column(BigInteger, ForeignKey("poll_option.id"))
- option: Option = relationship("Option", back_populates="votes", cascade="all, delete")
- vote_weight: Union[Column, float] = Column(Float)
-
-
class Option(Base):
__tablename__ = "poll_option"
@@ -59,6 +82,16 @@ async def create(poll: int, emote: str, option_text: str) -> Option:
return options
+class Voted(Base):
+ __tablename__ = "voted_user"
+
+ id: Union[Column, int] = Column(BigInteger, primary_key=True, autoincrement=True, unique=True)
+ user_id: Union[Column, int] = Column(BigInteger)
+ option_id: Union[Column, int] = Column(BigInteger, ForeignKey("poll_option.id"))
+ option: Option = relationship("Option", back_populates="votes", cascade="all, delete")
+ vote_weight: Union[Column, float] = Column(Float)
+
+
class RoleWeight(Base):
__tablename__ = "role_weight"
diff --git a/general/polls/translations/en.yml b/general/polls/translations/en.yml
index 2d0fca844..0f6ac2ab6 100644
--- a/general/polls/translations/en.yml
+++ b/general/polls/translations/en.yml
@@ -96,6 +96,7 @@ yes_no:
poll: Poll
team_poll: Team Poll
+team_yn_poll: Team Yes-No Poll
vote_explanation: Vote using the reactions below!
too_many_options: You specified too many options. The maximum amount is {}.
option_too_long: Options are limited to {} characters.
From 9c8615da9d4955b916b0550d644675ecca29addb Mon Sep 17 00:00:00 2001
From: NekoFanatic <83883849+NekoFanatic@users.noreply.github.com>
Date: Wed, 13 Apr 2022 01:25:41 +0200
Subject: [PATCH 10/44] Fixed mistake
---
general/polls/cog.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/general/polls/cog.py b/general/polls/cog.py
index fd393ae30..bf01e851e 100644
--- a/general/polls/cog.py
+++ b/general/polls/cog.py
@@ -168,8 +168,8 @@ async def callback(self, interaction):
poll = await db.get(TeamYesNo, message_id=message.id)
- if not (user := await db.get(YesNoUser, poll_id=message.id)):
- user = await YesNoUser.create(interaction.user.id, message.id, int(self.values[0]))
+ if not (user := await db.get(YesNoUser, poll_id=message.id, user=interaction.user.id)):
+ await YesNoUser.create(interaction.user.id, message.id, int(self.values[0]))
if int(self.values[0]) == 0:
poll.in_favor += 1
elif int(self.values[0]) == 1:
From 112caccdbd71d49b246b35373392904369094997 Mon Sep 17 00:00:00 2001
From: NekoFanatic <83883849+NekoFanatic@users.noreply.github.com>
Date: Wed, 13 Apr 2022 17:58:03 +0200
Subject: [PATCH 11/44] Added team-yes-no
---
general/polls/cog.py | 60 +++++++++++++------------------
general/polls/translations/en.yml | 1 +
2 files changed, 25 insertions(+), 36 deletions(-)
diff --git a/general/polls/cog.py b/general/polls/cog.py
index bf01e851e..68676aaf8 100644
--- a/general/polls/cog.py
+++ b/general/polls/cog.py
@@ -35,8 +35,8 @@
default_emojis = [name_to_emoji[f"regional_indicator_{x}"] for x in string.ascii_lowercase]
-def create_select_view(select: Select) -> View:
- view = View()
+def create_select_view(select: Select, timeout: float = None) -> View:
+ view = View(timeout=timeout)
view.add_item(select)
return view
@@ -158,12 +158,13 @@ async def get_teamler(guild: Guild, team_roles: list[str]) -> set[Member]:
class MySelect(Select):
@db_wrapper
async def callback(self, interaction):
- message: Message = interaction.message
+ message: Message = await interaction.channel.fetch_message(interaction.custom_id)
teamlers: set[Member] = await get_teamler(interaction.guild, ["team"])
team_poll = await get_teampoll_embed(message)
if team_poll[0] == t.team_yn_poll:
if interaction.user not in teamlers:
+ await interaction.response.send_message(content=t.team_yn_poll_forbidden, ephemeral=True)
return
poll = await db.get(TeamYesNo, message_id=message.id)
@@ -201,7 +202,8 @@ async def callback(self, interaction):
await message.edit(embed=embed)
elif team_poll[0] == t.team_poll:
- if interaction.user.id not in teamlers:
+ if interaction.user not in teamlers:
+ await interaction.response.send_message(content=t.team_yn_poll_forbidden, ephemeral=True)
return
else:
@@ -220,31 +222,6 @@ class PollsCog(Cog, name="Polls"):
def __init__(self, team_roles: list[str]):
self.team_roles: list[str] = team_roles
- async def get_reacted_teamlers(self, message: Optional[Message] = None) -> str:
- guild: Guild = self.bot.guilds[0]
-
- teamlers: set[Member] = set()
- for role_name in self.team_roles:
- if not (team_role := guild.get_role(await RoleSettings.get(role_name))):
- continue
-
- teamlers.update(member for member in team_role.members if not member.bot)
-
- if message:
- for reaction in message.reactions:
- if reaction.me:
- teamlers.difference_update(await reaction.users().flatten())
-
- teamlers: list[Member] = list(teamlers)
- if not teamlers:
- return t.teampoll_all_voted
-
- teamlers.sort(key=lambda m: str(m).lower())
-
- *teamlers, last = (x.mention for x in teamlers)
- teamlers: list[str]
- return t.teamlers_missing(teamlers=", ".join(teamlers), last=last, cnt=len(teamlers) + 1)
-
@commands.group(name="poll", aliases=["vote"])
@guild_only()
@docs(t.commands.poll.poll)
@@ -428,20 +405,31 @@ async def yesno(self, ctx: Context, message: Optional[Message] = None, text: Opt
@docs(t.commands.team_yes_no)
async def team_yesno(self, ctx: Context, *, text: str):
ops = [(t.yes_no.in_favor, "thumbsup", 0), (t.yes_no.against, "thumbsdown", 1), (t.yes_no.abstention, "zzz", 2)]
- select = MySelect(
- placeholder=t.select.placeholder(cnt=1),
- options=[SelectOption(label=op[0], emoji=name_to_emoji[op[1]], value=str(op[2])) for op in ops],
- )
+
embed = Embed(title=t.team_yn_poll, description=text, color=Colors.Polls, timestamp=utcnow())
embed.set_author(name=str(ctx.author), icon_url=ctx.author.display_avatar.url)
embed.add_field(name=t.yes_no.in_favor, value=t.yes_no.count(0, cnt=0), inline=True)
embed.add_field(name=t.yes_no.against, value=t.yes_no.count(0, cnt=0), inline=True)
embed.add_field(name=t.yes_no.abstention, value=t.yes_no.count(0, cnt=0), inline=True)
- embed.add_field(name=tg.status, value=await self.get_reacted_teamlers(), inline=False)
+ missing = list(await get_teamler(self.bot.guilds[0], ["team"]))
+ missing.sort(key=lambda m: str(m).lower())
+ *teamlers, last = (x.mention for x in missing)
+ teamlers: list[str]
+ embed.add_field(
+ name=tg.status,
+ value=t.teamlers_missing(teamlers=", ".join(teamlers), last=last, cnt=len(teamlers) + 1),
+ inline=False,
+ )
- msg: Message = await ctx.send(embed=embed, view=create_select_view(select))
- await TeamYesNo.create(msg.id)
+ embed_msg: Message = await ctx.send(embed=embed)
+ select = MySelect(
+ custom_id=str(embed_msg.id),
+ placeholder=t.select.placeholder(cnt=1),
+ options=[SelectOption(label=op[0], emoji=name_to_emoji[op[1]], value=str(op[2])) for op in ops],
+ )
+ await ctx.send(view=create_select_view(select))
+ await TeamYesNo.create(embed_msg.id)
class PollOption:
diff --git a/general/polls/translations/en.yml b/general/polls/translations/en.yml
index 0f6ac2ab6..3693f671c 100644
--- a/general/polls/translations/en.yml
+++ b/general/polls/translations/en.yml
@@ -97,6 +97,7 @@ yes_no:
poll: Poll
team_poll: Team Poll
team_yn_poll: Team Yes-No Poll
+team_yn_poll_forbidden: You are not allowed to use a team poll
vote_explanation: Vote using the reactions below!
too_many_options: You specified too many options. The maximum amount is {}.
option_too_long: Options are limited to {} characters.
From b2ccd7537bfab629846f52b256a3f40bcb991b01 Mon Sep 17 00:00:00 2001
From: NekoFanatic <83883849+NekoFanatic@users.noreply.github.com>
Date: Wed, 13 Apr 2022 23:21:00 +0200
Subject: [PATCH 12/44] Created wizard + argparse
---
general/polls/cog.py | 176 +++++++++++++++++++++---------
general/polls/models.py | 1 -
general/polls/translations/en.yml | 52 ++++++++-
3 files changed, 172 insertions(+), 57 deletions(-)
diff --git a/general/polls/cog.py b/general/polls/cog.py
index 68676aaf8..25b06b7b6 100644
--- a/general/polls/cog.py
+++ b/general/polls/cog.py
@@ -1,9 +1,10 @@
import re
import string
-from argparse import ArgumentParser
+from argparse import ArgumentParser, Namespace
from datetime import datetime
from typing import Optional, Tuple, Union
+from dateutil.relativedelta import relativedelta
from discord import Embed, Forbidden, Guild, Member, Message, Role, SelectOption
from discord.ext import commands
from discord.ext.commands import CommandError, Context, UserInputError, guild_only
@@ -35,6 +36,37 @@
default_emojis = [name_to_emoji[f"regional_indicator_{x}"] for x in string.ascii_lowercase]
+class PollOption:
+ def __init__(self, ctx: Context, line: str, number: int):
+ if not line:
+ raise CommandError(t.empty_option)
+
+ emoji_candidate, *text = line.lstrip().split(" ")
+ text = " ".join(text)
+
+ custom_emoji_match = re.fullmatch(r"", emoji_candidate)
+ if custom_emoji := ctx.bot.get_emoji(int(custom_emoji_match.group(1))) if custom_emoji_match else None:
+ self.emoji = custom_emoji
+ self.option = text.strip()
+ elif (unicode_emoji := emoji_candidate) in emoji_to_name:
+ self.emoji = unicode_emoji
+ self.option = text.strip()
+ elif (match := re.match(r"^:([^: ]+):$", emoji_candidate)) and (
+ unicode_emoji := name_to_emoji.get(match.group(1).replace(":", ""))
+ ):
+ self.emoji = unicode_emoji
+ self.option = text.strip()
+ else:
+ self.emoji = default_emojis[number]
+ self.option = line
+
+ if name_to_emoji["wastebasket"] == self.emoji:
+ raise CommandError(t.can_not_use_wastebucket_as_option)
+
+ def __str__(self):
+ return f"{self.emoji} {self.option}" if self.option else self.emoji
+
+
def create_select_view(select: Select, timeout: float = None) -> View:
view = View(timeout=timeout)
view.add_item(select)
@@ -48,13 +80,25 @@ def get_percentage(values: list[float]) -> list[tuple[float, float]]:
return [(value, round(((value / together) * 100), 2)) for value in values]
+def build_wizard(skip: bool = False) -> Embed:
+ if skip:
+ return Embed(title=t.skip.title, description=t.skip.description, color=Colors.Polls)
+
+ embed = Embed(title=t.wizard.title, description=t.wizard.description, color=Colors.Polls)
+ embed.add_field(name=t.wizard.arg, value=t.wizard.args, inline=False)
+ embed.add_field(name=t.wizard.example.name, value=t.wizard.example.value, inline=False)
+ embed.add_field(name=t.wizard.skip.name, value=t.wizard.skip.value, inline=False)
+
+ return embed
+
+
async def get_parser() -> ArgumentParser:
parser = ArgumentParser()
parser.add_argument("--type", "-T", default="standard", choices=["standard", "team"], type=str)
+ parser.add_argument("--deadline", "-D", default=await PollsDefaultSettings.duration.get(), type=int)
parser.add_argument(
- "--deadline", "-D", default=await PollsDefaultSettings.duration.get(), type=Union[int, datetime]
+ "--anonymous", "-A", default=await PollsDefaultSettings.anonymous.get(), type=bool, choices=[True, False]
)
- parser.add_argument("--anonymous", "-A", default=await PollsDefaultSettings.anonymous.get(), type=bool)
parser.add_argument("--choices", "-C", default=await PollsDefaultSettings.max_choices.get(), type=int)
return parser
@@ -71,38 +115,53 @@ async def get_teampoll_embed(message: Message) -> Tuple[Optional[str], Optional[
async def send_poll(
ctx: Context,
title: str,
- args: str,
+ poll_args: str,
max_choices: int = None,
field: Optional[Tuple[str, str]] = None,
- allow_delete: bool = True,
-):
- question, *options = [line.replace("\x00", "\n") for line in args.replace("\\\n", "\x00").split("\n") if line]
+ deadline: float = 0,
+) -> tuple[Message, list[PollOption]]:
+ if deadline != 0:
+ end_time: Optional[datetime] = datetime.today() + relativedelta(hours=int(deadline))
+ else:
+ end_time = None
+
+ if not max_choices or max_choices == 0:
+ max_choices = t.poll_config.choices.unlimited
+
+ question, *options = [line.replace("\x00", "\n") for line in poll_args.replace("\\\n", "\x00").split("\n") if line]
if not options:
raise CommandError(t.missing_options)
if len(options) > MAX_OPTIONS:
raise CommandError(t.too_many_options(MAX_OPTIONS))
+ if field and len(options) >= MAX_OPTIONS:
+ raise CommandError(t.too_many_options(MAX_OPTIONS - 1))
options = [PollOption(ctx, line, i) for i, line in enumerate(options)]
if any(len(str(option)) > EmbedLimits.FIELD_VALUE for option in options):
raise CommandError(t.option_too_long(EmbedLimits.FIELD_VALUE))
- embed = Embed(title=title, description=question, color=Colors.Polls, timestamp=utcnow())
+ if isinstance(max_choices, str) or await PollsDefaultSettings.max_choices.get() == 0 or len(options) == max_choices:
+ embed = Embed(title=t.title.poll.un(title), description=question, color=Colors.Polls, timestamp=utcnow())
+ else:
+ embed = Embed(
+ title=t.title.poll.mo(title, cnt=max_choices), description=question, color=Colors.Polls, timestamp=utcnow()
+ )
embed.set_author(name=str(ctx.author), icon_url=ctx.author.display_avatar.url)
- if allow_delete:
- embed.set_footer(text=t.created_by(ctx.author, ctx.author.id), icon_url=ctx.author.display_avatar.url)
+ if end_time:
+ embed.set_footer(text=t.footer(end_time.strftime("%Y-%m-%d %H:%M:%S")))
if len(set(map(lambda x: x.emoji, options))) < len(options):
raise CommandError(t.option_duplicated)
for option in options:
- embed.add_field(name="** **", value=str(option), inline=False)
+ embed.add_field(name=t.option.field.name(0, 0.0), value=str(option), inline=False)
if field:
embed.add_field(name=field[0], value=field[1], inline=False)
- if not max_choices:
+ if not max_choices or isinstance(max_choices, str):
place = t.select.place
max_value = len(options)
else:
@@ -110,7 +169,9 @@ async def send_poll(
place: str = t.select.placeholder(cnt=use)
max_value = use
+ msg = await ctx.send(embed=embed)
select = MySelect(
+ custom_id=str(msg.id),
placeholder=place,
max_values=max_value,
options=[
@@ -118,8 +179,9 @@ async def send_poll(
for index, option in enumerate(options)
],
)
+ await ctx.send(view=create_select_view(select))
- await ctx.send(embed=embed, view=create_select_view(select))
+ return msg, options
async def edit_team_yn(embed: Embed, poll: TeamYesNo, missing: list[Member]) -> Embed:
@@ -363,20 +425,65 @@ async def everyone(self, ctx: Context, weight: float = None):
await add_reactions(ctx.message, "white_check_mark")
await send_to_changelog(ctx.guild, msg)
- @poll.command(name="quick", usage=t.poll_usage, aliases=["q"])
+ @poll.command(name="quick", usage=t.usage.poll, aliases=["q"])
@docs(t.commands.poll.quick)
async def quick(self, ctx: Context, *, args: str):
- await send_poll(ctx, t.poll, args, await PollsDefaultSettings.max_choices.get())
+ await send_poll(
+ ctx=ctx,
+ title=t.poll,
+ poll_args=args,
+ max_choices=await PollsDefaultSettings.max_choices.get(),
+ deadline=await PollsDefaultSettings.duration.get(),
+ )
- @poll.command(name="new", usage=t.usage.new)
+ @poll.command(name="new", usage=t.usage.poll)
@docs(t.commands.poll.new)
- async def new(self, ctx: Context, *, args: str = None):
- parser = await get_parser()
- parsed = parser.parse_known_args(args)
+ async def new(self, ctx: Context, *, options: str):
+ def check(m: Message):
+ return m.author == ctx.author
+
+ wizard = await ctx.send(embed=build_wizard())
+ mess: Message = await self.bot.wait_for("message", check=check, timeout=60.0)
+ args = mess.content
+ if args.lower() == t.skip.message:
+ await wizard.edit(embed=build_wizard(True), delete_after=5.0)
+ else:
+ await wizard.delete(delay=5.0)
+ await mess.delete()
+
+ parser = await get_parser()
+ parsed: Namespace = parser.parse_known_args(args.split(" "))[0]
print(parsed)
+ poll_type: str = parsed.type
+ print(poll_type)
+ if poll_type.lower() == "team" and not await PollsPermission.team_poll.check_permissions(ctx.author):
+ poll_type = "standard"
+ deadline: Union[list[str, str], int] = parsed.deadline
+ if isinstance(deadline, int):
+ deadline: int = deadline
+ else:
+ deadline = await PollsDefaultSettings.duration.get() # TODO implement parsing for datetimes
+ anonymous: bool = parsed.anonymous
+ print(anonymous)
+ choices: int = parsed.choices
+
+ if poll_type.lower() == "team":
+ missing = list(await get_teamler(self.bot.guilds[0], ["team"]))
+ missing.sort(key=lambda m: str(m).lower())
+ *teamlers, last = (x.mention for x in missing)
+ teamlers: list[str]
+ field = (tg.status, t.teamlers_missing(teamlers=", ".join(teamlers), last=last, cnt=len(teamlers) + 1))
+ else:
+ field = None
+
+ poll_message, parsed_options = await send_poll(
+ ctx=ctx, title=t.poll, poll_args=options, max_choices=choices, field=field, deadline=deadline
+ )
+ await ctx.message.delete()
+
@commands.command(aliases=["yn"])
@guild_only()
@docs(t.commands.yes_no)
@@ -430,34 +537,3 @@ async def team_yesno(self, ctx: Context, *, text: str):
)
await ctx.send(view=create_select_view(select))
await TeamYesNo.create(embed_msg.id)
-
-
-class PollOption:
- def __init__(self, ctx: Context, line: str, number: int):
- if not line:
- raise CommandError(t.empty_option)
-
- emoji_candidate, *text = line.lstrip().split(" ")
- text = " ".join(text)
-
- custom_emoji_match = re.fullmatch(r"", emoji_candidate)
- if custom_emoji := ctx.bot.get_emoji(int(custom_emoji_match.group(1))) if custom_emoji_match else None:
- self.emoji = custom_emoji
- self.option = text.strip()
- elif (unicode_emoji := emoji_candidate) in emoji_to_name:
- self.emoji = unicode_emoji
- self.option = text.strip()
- elif (match := re.match(r"^:([^: ]+):$", emoji_candidate)) and (
- unicode_emoji := name_to_emoji.get(match.group(1).replace(":", ""))
- ):
- self.emoji = unicode_emoji
- self.option = text.strip()
- else:
- self.emoji = default_emojis[number]
- self.option = line
-
- if name_to_emoji["wastebasket"] == self.emoji:
- raise CommandError(t.can_not_use_wastebucket_as_option)
-
- def __str__(self):
- return f"{self.emoji} {self.option}" if self.option else self.emoji
diff --git a/general/polls/models.py b/general/polls/models.py
index aed3d515b..9a1688211 100644
--- a/general/polls/models.py
+++ b/general/polls/models.py
@@ -58,7 +58,6 @@ class Poll(Base):
poll_type: Union[Column, str] = Column(Text(50))
end_time: Union[Column, datetime] = Column(UTCDateTime)
anonymous: Union[Column, bool] = Column(Boolean)
- votes_amount: Union[Column, int] = Column(BigInteger)
poll_open: Union[Column, bool] = Column(Boolean)
can_delete: Union[Column, bool] = Column(Boolean)
keep: Union[Column, bool] = Column(Boolean)
diff --git a/general/polls/translations/en.yml b/general/polls/translations/en.yml
index 3693f671c..669e328db 100644
--- a/general/polls/translations/en.yml
+++ b/general/polls/translations/en.yml
@@ -84,7 +84,10 @@ select:
label: "Option {}."
usage:
- new: "[--type {standard,team}] [--deadline DEADLINE] [--anonymous ANONYMOUS] [--choices CHOICES]"
+ poll: |
+
+ [emoji1]
+ [emojiX] [optionX]
yes_no:
in_favor: "Yes"
@@ -94,6 +97,47 @@ yes_no:
one: "{cnt} vote ({}%)"
many: "{cnt} votes ({}%)"
+title:
+ poll:
+ un: "{} (multiple choice)"
+ mo:
+ one: "{} ({cnt} choice)"
+ many: "{} ({cnt} choices)"
+
+option:
+ field:
+ name: "**Voted: {}, Percentage: {}%**"
+
+skip:
+ message: skip
+ title: Skipped
+ description: Skipped poll wizard -> default poll created!
+
+wizard:
+ title: Poll wizard
+ description: Set arguments for an advanced poll
+ skip:
+ name: Skip setup
+ value: To skip the setup type `skip`
+ arg: Arguments
+ args: |
+ ```
+ --type {standard,team}, -T {standard,team}
+ standard or team embed [Default: 'standard']
+ --deadline DEADLINE, -D DEADLINE
+ time when the poll should be closed [Default: server settings]
+ --anonymous ANONYMOUS, -A ANONYMOUS
+ people can see who voted or not [Default: server settings]
+ --choices CHOICES, -C CHOICES
+ the amount of votes someone can set
+ ```
+ example:
+ name: Example
+ value: |
+ `--duration 6 --choices 4 -A True`
+
+ --> Creates an anonymous, 6 hours long poll with 4 select choices for every user
+
poll: Poll
team_poll: Team Poll
team_yn_poll: Team Yes-No Poll
@@ -104,17 +148,13 @@ option_too_long: Options are limited to {} characters.
missing_options: Missing options
option_duplicated: You may not use the same emoji twice!
empty_option: Empty option
-poll_usage: |
-
- [emoji1]
- [emojiX] [optionX]
team_role_not_set: Team role is not set.
team_role_no_members: The team role has no members.
teampoll_all_voted: "All teamlers voted :white_check_mark:"
teamlers_missing:
one: "{last} hasn't voted yet."
many: "{teamlers} and {last} haven't voted yet."
-created_by: Created by @{} ({})
+footer: Ends at `{}`
can_not_use_wastebucket_as_option: "You can not use :wastebasket: as option"
foreign_message: "You are not allowed to add yes/no reactions to foreign messages!"
could_not_add_reactions: Could not add reactions because I don't have `add_reactions` permission in {}.
From 9f1eb96c8ec43544b0d7ca12b18648d7422dbdbe Mon Sep 17 00:00:00 2001
From: NekoFanatic <83883849+NekoFanatic@users.noreply.github.com>
Date: Thu, 14 Apr 2022 19:14:17 +0200
Subject: [PATCH 13/44] some changes
---
general/polls/cog.py | 14 +++++---------
general/polls/translations/en.yml | 7 -------
2 files changed, 5 insertions(+), 16 deletions(-)
diff --git a/general/polls/cog.py b/general/polls/cog.py
index 25b06b7b6..9cc2734d2 100644
--- a/general/polls/cog.py
+++ b/general/polls/cog.py
@@ -142,12 +142,7 @@ async def send_poll(
if any(len(str(option)) > EmbedLimits.FIELD_VALUE for option in options):
raise CommandError(t.option_too_long(EmbedLimits.FIELD_VALUE))
- if isinstance(max_choices, str) or await PollsDefaultSettings.max_choices.get() == 0 or len(options) == max_choices:
- embed = Embed(title=t.title.poll.un(title), description=question, color=Colors.Polls, timestamp=utcnow())
- else:
- embed = Embed(
- title=t.title.poll.mo(title, cnt=max_choices), description=question, color=Colors.Polls, timestamp=utcnow()
- )
+ embed = Embed(title=title, description=question, color=Colors.Polls, timestamp=utcnow())
embed.set_author(name=str(ctx.author), icon_url=ctx.author.display_avatar.url)
if end_time:
embed.set_footer(text=t.footer(end_time.strftime("%Y-%m-%d %H:%M:%S")))
@@ -455,12 +450,13 @@ def check(m: Message):
parser = await get_parser()
parsed: Namespace = parser.parse_known_args(args.split(" "))[0]
- print(parsed)
+ title: str = t.team_poll
poll_type: str = parsed.type
- print(poll_type)
if poll_type.lower() == "team" and not await PollsPermission.team_poll.check_permissions(ctx.author):
poll_type = "standard"
+ if poll_type == "standard":
+ title: str = t.poll
deadline: Union[list[str, str], int] = parsed.deadline
if isinstance(deadline, int):
deadline: int = deadline
@@ -480,7 +476,7 @@ def check(m: Message):
field = None
poll_message, parsed_options = await send_poll(
- ctx=ctx, title=t.poll, poll_args=options, max_choices=choices, field=field, deadline=deadline
+ ctx=ctx, title=title, poll_args=options, max_choices=choices, field=field, deadline=deadline
)
await ctx.message.delete()
diff --git a/general/polls/translations/en.yml b/general/polls/translations/en.yml
index 669e328db..22405244c 100644
--- a/general/polls/translations/en.yml
+++ b/general/polls/translations/en.yml
@@ -97,13 +97,6 @@ yes_no:
one: "{cnt} vote ({}%)"
many: "{cnt} votes ({}%)"
-title:
- poll:
- un: "{} (multiple choice)"
- mo:
- one: "{} ({cnt} choice)"
- many: "{} ({cnt} choices)"
-
option:
field:
name: "**Voted: {}, Percentage: {}%**"
From 36fdb5102d15afb9ee216c7bd808c2e89324ce6d Mon Sep 17 00:00:00 2001
From: NekoFanatic <83883849+NekoFanatic@users.noreply.github.com>
Date: Fri, 15 Apr 2022 08:55:09 +0200
Subject: [PATCH 14/44] saving commit
---
general/polls/models.py | 41 ++++++++++++++++++++++++++++++++++++++++-
1 file changed, 40 insertions(+), 1 deletion(-)
diff --git a/general/polls/models.py b/general/polls/models.py
index 9a1688211..a1d8ced58 100644
--- a/general/polls/models.py
+++ b/general/polls/models.py
@@ -62,12 +62,46 @@ class Poll(Base):
can_delete: Union[Column, bool] = Column(Boolean)
keep: Union[Column, bool] = Column(Boolean)
+ @staticmethod
+ async def create(
+ message_id: int,
+ channel: int,
+ owner: int,
+ title: str,
+ options: list,
+ end: int,
+ anonymous: bool,
+ can_delete: bool,
+ keep: bool,
+ poll_type: str,
+ ) -> Poll:
+ row = Poll(
+ message_id=message_id,
+ poll_channel=channel,
+ owner_id=owner,
+ timestamp=utcnow(),
+ title=title,
+ poll_type=poll_type,
+ end=end,
+ anonymous=anonymous,
+ can_delete=can_delete,
+ keep=keep,
+ )
+ for poll_option in options:
+ await Option.create(poll=message_id, emote=poll_option.emoji, option_text=poll_option.option)
+
+ await db.add(row)
+ return row
+
+ async def remove(self):
+ await db.delete(self)
+
class Option(Base):
__tablename__ = "poll_option"
id: Union[Column, int] = Column(BigInteger, primary_key=True, autoincrement=True, unique=True)
- poll_id: Union[Column, int] = Column(BigInteger, ForeignKey("poll.id"))
+ poll_id: Union[Column, int] = Column(BigInteger, ForeignKey("poll.message_id"))
votes: list[Voted] = relationship("Voted", back_populates="option")
poll: Poll = relationship("Poll", back_populates="options")
emote: Union[Column, str] = Column(Text(30))
@@ -90,6 +124,11 @@ class Voted(Base):
option: Option = relationship("Option", back_populates="votes", cascade="all, delete")
vote_weight: Union[Column, float] = Column(Float)
+ @staticmethod
+ async def create(user_id: int, option_id: int, vote_weight: float):
+ row = Voted(user_id=user_id, option_id=option_id, vote_weight=vote_weight)
+ await db.add(row)
+
class RoleWeight(Base):
__tablename__ = "role_weight"
From e749fd010ad9605c03fe7fe9f27b0f4a14d1150c Mon Sep 17 00:00:00 2001
From: NekoFanatic <83883849+NekoFanatic@users.noreply.github.com>
Date: Fri, 15 Apr 2022 09:03:04 +0200
Subject: [PATCH 15/44] removed 'hidden' command (useless)
---
general/polls/cog.py | 17 +----------------
general/polls/settings.py | 1 -
general/polls/translations/en.yml | 8 +-------
3 files changed, 2 insertions(+), 24 deletions(-)
diff --git a/general/polls/cog.py b/general/polls/cog.py
index 9cc2734d2..31878d2e5 100644
--- a/general/polls/cog.py
+++ b/general/polls/cog.py
@@ -142,7 +142,7 @@ async def send_poll(
if any(len(str(option)) > EmbedLimits.FIELD_VALUE for option in options):
raise CommandError(t.option_too_long(EmbedLimits.FIELD_VALUE))
- embed = Embed(title=title, description=question, color=Colors.Polls, timestamp=utcnow())
+ embed = Embed(title=title, description=t.poll_titles(question), color=Colors.Polls, timestamp=utcnow())
embed.set_author(name=str(ctx.author), icon_url=ctx.author.display_avatar.url)
if end_time:
embed.set_footer(text=t.footer(end_time.strftime("%Y-%m-%d %H:%M:%S")))
@@ -308,8 +308,6 @@ async def settings(self, ctx: Context):
value=t.poll_config.choices.amount(cnt=choice) if not choice <= 0 else t.poll_config.choices.unlimited,
inline=False,
)
- hide: bool = await PollsDefaultSettings.hidden.get()
- embed.add_field(name=t.poll_config.hidden.name, value=str(hide), inline=False)
anonymous: bool = await PollsDefaultSettings.anonymous.get()
embed.add_field(name=t.poll_config.anonymous.name, value=str(anonymous), inline=False)
roles = await RoleWeight.get()
@@ -377,19 +375,6 @@ async def votes(self, ctx: Context, votes: int = None):
await add_reactions(ctx.message, "white_check_mark")
await send_to_changelog(ctx.guild, msg)
- @settings.command(name="hidden", aliases=["h"])
- @PollsPermission.write.check
- @docs(t.commands.poll.settings.hidden)
- async def hidden(self, ctx: Context, status: bool):
- if status:
- msg: str = t.hidden.hidden
- else:
- msg: str = t.hidden.not_hidden
-
- await PollsDefaultSettings.hidden.set(status)
- await add_reactions(ctx.message, "white_check_mark")
- await send_to_changelog(ctx.guild, msg)
-
@settings.command(name="anonymous", aliases=["a"])
@PollsPermission.write.check
@docs(t.commands.poll.settings.anonymous)
diff --git a/general/polls/settings.py b/general/polls/settings.py
index 6937bf6dd..775fe0f14 100644
--- a/general/polls/settings.py
+++ b/general/polls/settings.py
@@ -5,6 +5,5 @@ class PollsDefaultSettings(Settings):
duration = 0 # 0 for unlimited duration (duration in hours)
max_choices = 0 # 0 for unlimited choices
type = "standard"
- hidden = False
everyone_power = 1.0
anonymous = False
diff --git a/general/polls/translations/en.yml b/general/polls/translations/en.yml
index 22405244c..c53c1cf1e 100644
--- a/general/polls/translations/en.yml
+++ b/general/polls/translations/en.yml
@@ -8,7 +8,6 @@ commands:
roles_weights: manage weight for certain roles
duration: set the default hours a poll should be open
votes: set the default amount of votes a user can have on polls
- hidden: set hide attribute for votes on polls
anonymous: set if user can see who voted on a poll
everyone: manage role weight for the default role
yes_no: add thumbs-up/down emotes on a message
@@ -39,8 +38,6 @@ poll_config:
one: "{cnt} choice per user"
many: "{cnt} choices per user"
unlimited: unlimited
- hidden:
- name: "**Hidden Votes**"
anonymous:
name: "Anonymous"
roles:
@@ -68,10 +65,6 @@ votes:
many: "Set default votes for a poll to {cnt} votes"
reset: "Set the default votes for polls to unlimited"
-hidden:
- hidden: made votes hidden
- not_hidden: made votes visible
-
anonymous:
is_on: made default poll votes anonymous
is_off: made default poll votes visible
@@ -131,6 +124,7 @@ wizard:
--> Creates an anonymous, 6 hours long poll with 4 select choices for every user
+poll_titles: "**`{}`**"
poll: Poll
team_poll: Team Poll
team_yn_poll: Team Yes-No Poll
From 293a032171d89c6e85bbcc72777633e44cf4b029 Mon Sep 17 00:00:00 2001
From: NekoFanatic <83883849+NekoFanatic@users.noreply.github.com>
Date: Wed, 27 Apr 2022 15:44:32 +0200
Subject: [PATCH 16/44] If u look at this code, its ur fault
---
general/polls/cog.py | 188 ++++++++++++++----------------
general/polls/models.py | 85 ++++++--------
general/polls/translations/en.yml | 5 +-
3 files changed, 125 insertions(+), 153 deletions(-)
diff --git a/general/polls/cog.py b/general/polls/cog.py
index 31878d2e5..9fbb42584 100644
--- a/general/polls/cog.py
+++ b/general/polls/cog.py
@@ -2,7 +2,7 @@
import string
from argparse import ArgumentParser, Namespace
from datetime import datetime
-from typing import Optional, Tuple, Union
+from typing import Optional, Union
from dateutil.relativedelta import relativedelta
from discord import Embed, Forbidden, Guild, Member, Message, Role, SelectOption
@@ -21,7 +21,7 @@
from PyDrocsid.util import is_teamler
from .colors import Colors
-from .models import RoleWeight, TeamYesNo, YesNoUser
+from .models import Option, Poll, RoleWeight, Voted
from .permissions import PollsPermission
from .settings import PollsDefaultSettings
from ...contributor import Contributor
@@ -60,9 +60,6 @@ def __init__(self, ctx: Context, line: str, number: int):
self.emoji = default_emojis[number]
self.option = line
- if name_to_emoji["wastebasket"] == self.emoji:
- raise CommandError(t.can_not_use_wastebucket_as_option)
-
def __str__(self):
return f"{self.emoji} {self.option}" if self.option else self.emoji
@@ -104,12 +101,10 @@ async def get_parser() -> ArgumentParser:
return parser
-async def get_teampoll_embed(message: Message) -> Tuple[Optional[str], Optional[Embed], Optional[int]]:
- for embed in message.embeds:
- for i, field in enumerate(embed.fields):
- if tg.status == field.name:
- return embed.title, embed, i
- return None, None, None
+def calc_end_time(duration: Optional[float]) -> Optional[datetime]:
+ if duration != 0 and not None:
+ return datetime.today() + relativedelta(hours=int(duration))
+ return
async def send_poll(
@@ -117,13 +112,9 @@ async def send_poll(
title: str,
poll_args: str,
max_choices: int = None,
- field: Optional[Tuple[str, str]] = None,
- deadline: float = 0,
-) -> tuple[Message, list[PollOption]]:
- if deadline != 0:
- end_time: Optional[datetime] = datetime.today() + relativedelta(hours=int(deadline))
- else:
- end_time = None
+ field: Optional[tuple[str, str]] = None,
+ deadline: Optional[float] = None,
+) -> tuple[Message, Message, list[tuple[str, str]], str]:
if not max_choices or max_choices == 0:
max_choices = t.poll_config.choices.unlimited
@@ -142,8 +133,10 @@ async def send_poll(
if any(len(str(option)) > EmbedLimits.FIELD_VALUE for option in options):
raise CommandError(t.option_too_long(EmbedLimits.FIELD_VALUE))
- embed = Embed(title=title, description=t.poll_titles(question), color=Colors.Polls, timestamp=utcnow())
+ embed = Embed(title=title, description=question, color=Colors.Polls, timestamp=utcnow())
embed.set_author(name=str(ctx.author), icon_url=ctx.author.display_avatar.url)
+
+ end_time = calc_end_time(deadline)
if end_time:
embed.set_footer(text=t.footer(end_time.strftime("%Y-%m-%d %H:%M:%S")))
@@ -174,20 +167,19 @@ async def send_poll(
for index, option in enumerate(options)
],
)
- await ctx.send(view=create_select_view(select))
+ view_msg = await ctx.send(view=create_select_view(select=select, timeout=deadline))
+ parsed_options: list[tuple[str, str]] = [
+ (obj.emoji, t.select.label(index + 1)) for index, obj in enumerate(options)
+ ]
- return msg, options
+ return msg, view_msg, parsed_options, question
-async def edit_team_yn(embed: Embed, poll: TeamYesNo, missing: list[Member]) -> Embed:
- calc = get_percentage([poll.in_favor, poll.against, poll.abstention])
+async def edit_poll_embed(embed: Embed, poll: Poll, missing: list[Member] = None) -> Embed:
+ calc = get_percentage([]) # TODO: option votes
+ print(calc)
for index, field in enumerate(embed.fields):
- if field.name == t.yes_no.in_favor:
- embed.set_field_at(index, name=field.name, value=t.yes_no.count(calc[0][1], cnt=calc[0][0]))
- elif field.name == t.yes_no.against:
- embed.set_field_at(index, name=field.name, value=t.yes_no.count(calc[1][1], cnt=calc[1][0]))
- elif field.name == t.yes_no.abstention:
- embed.set_field_at(index, name=field.name, value=t.yes_no.count(calc[2][1], cnt=calc[2][0]))
+ # TODO hier errechnen
if field.name == tg.status:
missing.sort(key=lambda m: str(m).lower())
*teamlers, last = (x.mention for x in missing)
@@ -215,56 +207,39 @@ async def get_teamler(guild: Guild, team_roles: list[str]) -> set[Member]:
class MySelect(Select):
@db_wrapper
async def callback(self, interaction):
+ user = interaction.user
+ selected_options: list = self.values
message: Message = await interaction.channel.fetch_message(interaction.custom_id)
- teamlers: set[Member] = await get_teamler(interaction.guild, ["team"])
- team_poll = await get_teampoll_embed(message)
+ embed: Embed = message.embeds[0] if message.embeds else None
+ poll: Poll = await db.get(Poll, message_id=message.id)
+ if not poll or not embed:
+ return
- if team_poll[0] == t.team_yn_poll:
- if interaction.user not in teamlers:
- await interaction.response.send_message(content=t.team_yn_poll_forbidden, ephemeral=True)
- return
+ options: list[Option] = await poll.get_options()
+ options: list[Option] = [option for option in options if option.option in selected_options]
+ missing: list[Member] = []
+ print(missing)
- poll = await db.get(TeamYesNo, message_id=message.id)
-
- if not (user := await db.get(YesNoUser, poll_id=message.id, user=interaction.user.id)):
- await YesNoUser.create(interaction.user.id, message.id, int(self.values[0]))
- if int(self.values[0]) == 0:
- poll.in_favor += 1
- elif int(self.values[0]) == 1:
- poll.against += 1
- else:
- poll.abstention += 1
- else:
- old_user_option = int(user.option)
- user.option = int(self.values[0])
- if int(self.values[0]) == 0:
- poll.in_favor += 1
- elif int(self.values[0]) == 1:
- poll.against += 1
- else:
- poll.abstention += 1
-
- if old_user_option == 0:
- poll.in_favor -= 1
- elif old_user_option == 1:
- poll.against -= 1
- else:
- poll.abstention -= 1
-
- rows = await db.all(filter_by(YesNoUser, poll_id=message.id))
- user_ids = [user.user for user in rows]
- missing: list[Member] = [team for team in teamlers if team.id not in user_ids]
-
- embed = await edit_team_yn(team_poll[1], poll, missing)
- await message.edit(embed=embed)
-
- elif team_poll[0] == t.team_poll:
- if interaction.user not in teamlers:
+ old_selected: list[Voted] = await db.all(filter_by(Voted, user_id=user.id, poll_id=message.id))
+ if old_selected:
+ for old in old_selected:
+ await old.remove()
+
+ if poll.fair:
+ user_weight: float = await PollsDefaultSettings.everyone_power.get()
+ else:
+ user_weight: float = 1.0 # TODO: Add function to get user vote weight
+ for option in options:
+ await Voted.create(option_id=option.id, user_id=user.id, poll_id=poll.id, vote_weight=user_weight)
+ print(await poll.get_voted_user())
+ if poll.poll_type == "team":
+ teamlers: set[Member] = await get_teamler(interaction.guild, ["team"])
+ missing: list[Member] = []
+ if user not in teamlers:
await interaction.response.send_message(content=t.team_yn_poll_forbidden, ephemeral=True)
return
- else:
- pass
+ # await edit_poll_embed(embed, poll, missing)
class PollsCog(Cog, name="Polls"):
@@ -408,13 +383,25 @@ async def everyone(self, ctx: Context, weight: float = None):
@poll.command(name="quick", usage=t.usage.poll, aliases=["q"])
@docs(t.commands.poll.quick)
async def quick(self, ctx: Context, *, args: str):
+ deadline = await PollsDefaultSettings.duration.get()
+ max_choices = await PollsDefaultSettings.max_choices.get()
+ anonymous = await PollsDefaultSettings.anonymous.get()
+ message, interaction, parsed_options, question = await send_poll(
+ ctx=ctx, title=t.poll, poll_args=args, max_choices=max_choices, deadline=deadline
+ )
- await send_poll(
- ctx=ctx,
- title=t.poll,
- poll_args=args,
- max_choices=await PollsDefaultSettings.max_choices.get(),
- deadline=await PollsDefaultSettings.duration.get(),
+ await Poll.create(
+ message_id=message.id,
+ channel=message.channel.id,
+ owner=ctx.author.id,
+ title=question,
+ end=calc_end_time(deadline),
+ anonymous=anonymous,
+ can_delete=True,
+ options=parsed_options,
+ poll_type="standard",
+ interaction=interaction.id,
+ fair=False,
)
@poll.command(name="new", usage=t.usage.poll)
@@ -423,6 +410,7 @@ async def new(self, ctx: Context, *, options: str):
def check(m: Message):
return m.author == ctx.author
+ print(options)
wizard = await ctx.send(embed=build_wizard())
mess: Message = await self.bot.wait_for("message", check=check, timeout=60.0)
args = mess.content
@@ -448,23 +436,38 @@ def check(m: Message):
else:
deadline = await PollsDefaultSettings.duration.get() # TODO implement parsing for datetimes
anonymous: bool = parsed.anonymous
- print(anonymous)
choices: int = parsed.choices
if poll_type.lower() == "team":
+ can_delete, fair = False, True
missing = list(await get_teamler(self.bot.guilds[0], ["team"]))
missing.sort(key=lambda m: str(m).lower())
*teamlers, last = (x.mention for x in missing)
teamlers: list[str]
field = (tg.status, t.teamlers_missing(teamlers=", ".join(teamlers), last=last, cnt=len(teamlers) + 1))
else:
+ can_delete, fair = True, False
field = None
- poll_message, parsed_options = await send_poll(
+ message, interaction, parsed_options, question = await send_poll(
ctx=ctx, title=title, poll_args=options, max_choices=choices, field=field, deadline=deadline
)
await ctx.message.delete()
+ await Poll.create(
+ message_id=message.id,
+ channel=message.channel.id,
+ owner=ctx.author.id,
+ title=question,
+ end=calc_end_time(deadline),
+ anonymous=anonymous,
+ can_delete=can_delete,
+ options=parsed_options,
+ poll_type=poll_type.lower(),
+ interaction=interaction.id,
+ fair=fair,
+ )
+
@commands.command(aliases=["yn"])
@guild_only()
@docs(t.commands.yes_no)
@@ -492,29 +495,14 @@ async def yesno(self, ctx: Context, message: Optional[Message] = None, text: Opt
@guild_only()
@docs(t.commands.team_yes_no)
async def team_yesno(self, ctx: Context, *, text: str):
- ops = [(t.yes_no.in_favor, "thumbsup", 0), (t.yes_no.against, "thumbsdown", 1), (t.yes_no.abstention, "zzz", 2)]
+ options = t.yes_no.option_string(text)
- embed = Embed(title=t.team_yn_poll, description=text, color=Colors.Polls, timestamp=utcnow())
- embed.set_author(name=str(ctx.author), icon_url=ctx.author.display_avatar.url)
-
- embed.add_field(name=t.yes_no.in_favor, value=t.yes_no.count(0, cnt=0), inline=True)
- embed.add_field(name=t.yes_no.against, value=t.yes_no.count(0, cnt=0), inline=True)
- embed.add_field(name=t.yes_no.abstention, value=t.yes_no.count(0, cnt=0), inline=True)
missing = list(await get_teamler(self.bot.guilds[0], ["team"]))
missing.sort(key=lambda m: str(m).lower())
*teamlers, last = (x.mention for x in missing)
teamlers: list[str]
- embed.add_field(
- name=tg.status,
- value=t.teamlers_missing(teamlers=", ".join(teamlers), last=last, cnt=len(teamlers) + 1),
- inline=False,
- )
+ field = (tg.status, t.teamlers_missing(teamlers=", ".join(teamlers), last=last, cnt=len(teamlers) + 1))
- embed_msg: Message = await ctx.send(embed=embed)
- select = MySelect(
- custom_id=str(embed_msg.id),
- placeholder=t.select.placeholder(cnt=1),
- options=[SelectOption(label=op[0], emoji=name_to_emoji[op[1]], value=str(op[2])) for op in ops],
+ embed_msg, interaction, parsed_options, question = await send_poll(
+ ctx=ctx, title=t.team_poll, max_choices=1, poll_args=options, field=field
)
- await ctx.send(view=create_select_view(select))
- await TeamYesNo.create(embed_msg.id)
diff --git a/general/polls/models.py b/general/polls/models.py
index a1d8ced58..69f4b191c 100644
--- a/general/polls/models.py
+++ b/general/polls/models.py
@@ -1,46 +1,13 @@
from __future__ import annotations
from datetime import datetime
-from typing import Union
+from typing import Optional, Union
from discord.utils import utcnow
from sqlalchemy import BigInteger, Boolean, Column, Float, ForeignKey, Text
from sqlalchemy.orm import relationship
-from PyDrocsid.database import Base, UTCDateTime, db, select
-
-
-class TeamYesNo(Base):
- __tablename__ = "team_yes_no"
-
- message_id: Union[Column, int] = Column(BigInteger, unique=True, primary_key=True)
- users: list[YesNoUser] = relationship("YesNoUser", back_populates="poll", cascade="all, delete")
- in_favor: Union[Column, int] = Column(Float)
- against: Union[Column, int] = Column(Float)
- abstention: Union[Column, int] = Column(Float)
- timestamp: Union[Column, datetime] = Column(UTCDateTime)
-
- @staticmethod
- async def create(message_id: int) -> TeamYesNo:
- row = TeamYesNo(message_id=message_id, in_favor=0, against=0, abstention=0, timestamp=utcnow())
- await db.add(row)
- return row
-
-
-class YesNoUser(Base):
- __tablename__ = "team_yes_no_voter"
-
- id: Union[Column, int] = Column(BigInteger, primary_key=True, autoincrement=True, unique=True)
- poll_id: Union[Column, int] = Column(BigInteger, ForeignKey("team_yes_no.message_id"))
- poll: TeamYesNo = relationship("TeamYesNo", back_populates="users")
- option: Union[Column, int] = Column(BigInteger)
- user: Union[Column, int] = Column(BigInteger)
-
- @staticmethod
- async def create(user: int, poll_id: int, option: int) -> YesNoUser:
- row = YesNoUser(user=user, poll_id=poll_id, option=option)
- await db.add(row)
- return row
+from PyDrocsid.database import Base, UTCDateTime, db, filter_by, select
class Poll(Base):
@@ -51,16 +18,16 @@ class Poll(Base):
options: list[Option] = relationship("Option", back_populates="poll", cascade="all, delete")
message_id: Union[Column, int] = Column(BigInteger, unique=True)
- poll_channel: Union[Column, int] = Column(BigInteger)
+ interaction_message_id: Union[Column, int] = Column(BigInteger, unique=True)
+ channel_id: Union[Column, int] = Column(BigInteger)
owner_id: Union[Column, int] = Column(BigInteger)
timestamp: Union[Column, datetime] = Column(UTCDateTime)
title: Union[Column, str] = Column(Text(256))
poll_type: Union[Column, str] = Column(Text(50))
end_time: Union[Column, datetime] = Column(UTCDateTime)
anonymous: Union[Column, bool] = Column(Boolean)
- poll_open: Union[Column, bool] = Column(Boolean)
can_delete: Union[Column, bool] = Column(Boolean)
- keep: Union[Column, bool] = Column(Boolean)
+ fair: Union[Column, bool] = Column(Boolean)
@staticmethod
async def create(
@@ -68,27 +35,31 @@ async def create(
channel: int,
owner: int,
title: str,
- options: list,
- end: int,
+ options: list[tuple[str, str]],
+ end: Optional[datetime],
anonymous: bool,
can_delete: bool,
- keep: bool,
poll_type: str,
+ interaction: int,
+ fair: bool,
) -> Poll:
row = Poll(
message_id=message_id,
- poll_channel=channel,
+ channel_id=channel,
owner_id=owner,
timestamp=utcnow(),
title=title,
poll_type=poll_type,
- end=end,
+ end_time=end,
anonymous=anonymous,
can_delete=can_delete,
- keep=keep,
+ interaction_message_id=interaction,
+ fair=fair,
)
- for poll_option in options:
- await Option.create(poll=message_id, emote=poll_option.emoji, option_text=poll_option.option)
+ for position, poll_option in enumerate(options):
+ await Option.create(
+ poll=message_id, emote=poll_option[0], option_text=poll_option[1], field_position=position
+ )
await db.add(row)
return row
@@ -96,6 +67,12 @@ async def create(
async def remove(self):
await db.delete(self)
+ async def get_options(self) -> list[Option]:
+ return list(await db.all(filter_by(Option, poll_id=self.message_id)))
+
+ async def get_voted_user(self):
+ return [await option.get_user() for option in await db.all(filter_by(Option, poll_id=self.message_id))]
+
class Option(Base):
__tablename__ = "poll_option"
@@ -106,14 +83,18 @@ class Option(Base):
poll: Poll = relationship("Poll", back_populates="options")
emote: Union[Column, str] = Column(Text(30))
option: Union[Column, str] = Column(Text(150))
+ field_position: Union[Column, int] = Column(BigInteger)
@staticmethod
- async def create(poll: int, emote: str, option_text: str) -> Option:
- options = Option(poll_id=poll, emote=emote, option=option_text)
+ async def create(poll: int, emote: str, option_text: str, field_position: int) -> Option:
+ options = Option(poll_id=poll, emote=emote, option=option_text, field_position=field_position)
await db.add(options)
return options
+ async def get_user(self) -> list[Voted]:
+ return list(await db.all(filter_by(Voted, option_id=self.id)))
+
class Voted(Base):
__tablename__ = "voted_user"
@@ -123,12 +104,16 @@ class Voted(Base):
option_id: Union[Column, int] = Column(BigInteger, ForeignKey("poll_option.id"))
option: Option = relationship("Option", back_populates="votes", cascade="all, delete")
vote_weight: Union[Column, float] = Column(Float)
+ poll_id: Union[Column, int] = Column(BigInteger)
@staticmethod
- async def create(user_id: int, option_id: int, vote_weight: float):
- row = Voted(user_id=user_id, option_id=option_id, vote_weight=vote_weight)
+ async def create(user_id: int, option_id: int, vote_weight: float, poll_id: int):
+ row = Voted(user_id=user_id, option_id=option_id, vote_weight=vote_weight, poll_id=poll_id)
await db.add(row)
+ async def remove(self):
+ await db.delete(self)
+
class RoleWeight(Base):
__tablename__ = "role_weight"
diff --git a/general/polls/translations/en.yml b/general/polls/translations/en.yml
index c53c1cf1e..60fda847f 100644
--- a/general/polls/translations/en.yml
+++ b/general/polls/translations/en.yml
@@ -86,13 +86,14 @@ yes_no:
in_favor: "Yes"
against: "No"
abstention: "Abstention"
+ option_string: "{}\n:thumbsup: Yes\n:thumbsdown: No\n:zzz: Abstention"
count:
one: "{cnt} vote ({}%)"
many: "{cnt} votes ({}%)"
option:
field:
- name: "**Voted: {}, Percentage: {}%**"
+ name: "**Votes: {} ({}%)**"
skip:
message: skip
@@ -124,10 +125,8 @@ wizard:
--> Creates an anonymous, 6 hours long poll with 4 select choices for every user
-poll_titles: "**`{}`**"
poll: Poll
team_poll: Team Poll
-team_yn_poll: Team Yes-No Poll
team_yn_poll_forbidden: You are not allowed to use a team poll
vote_explanation: Vote using the reactions below!
too_many_options: You specified too many options. The maximum amount is {}.
From 948848ca447e591fbde126e8f860412d2494d525 Mon Sep 17 00:00:00 2001
From: NekoFanatic <83883849+NekoFanatic@users.noreply.github.com>
Date: Wed, 4 May 2022 18:41:04 +0200
Subject: [PATCH 17/44] some changes
---
general/polls/cog.py | 64 +++++++++++++++++--------------
general/polls/models.py | 28 +++++---------
general/polls/translations/en.yml | 2 +-
3 files changed, 47 insertions(+), 47 deletions(-)
diff --git a/general/polls/cog.py b/general/polls/cog.py
index 9fbb42584..651987ef8 100644
--- a/general/polls/cog.py
+++ b/general/polls/cog.py
@@ -13,7 +13,7 @@
from PyDrocsid.cog import Cog
from PyDrocsid.command import add_reactions, docs
-from PyDrocsid.database import db, db_wrapper, filter_by
+from PyDrocsid.database import db, db_wrapper
from PyDrocsid.embeds import EmbedLimits, send_long_embed
from PyDrocsid.emojis import emoji_to_name, name_to_emoji
from PyDrocsid.settings import RoleSettings
@@ -21,7 +21,7 @@
from PyDrocsid.util import is_teamler
from .colors import Colors
-from .models import Option, Poll, RoleWeight, Voted
+from .models import Option, Poll, PollVote, RoleWeight
from .permissions import PollsPermission
from .settings import PollsDefaultSettings
from ...contributor import Contributor
@@ -64,17 +64,21 @@ def __str__(self):
return f"{self.emoji} {self.option}" if self.option else self.emoji
-def create_select_view(select: Select, timeout: float = None) -> View:
+def create_select_view(select_obj: Select, timeout: float = None) -> View:
view = View(timeout=timeout)
- view.add_item(select)
+ view.add_item(select_obj)
return view
-def get_percentage(values: list[float]) -> list[tuple[float, float]]:
- together = sum(values)
+def get_percentage(poll: Poll) -> list[tuple[float, float]]:
+ values: list[float] = []
+ options = poll.options
- return [(value, round(((value / together) * 100), 2)) for value in values]
+ for option in options:
+ values.append(sum([vote.vote_weight for vote in option.votes]))
+
+ return [(value, round(((value / sum(values)) * 100), 2)) for value in values]
def build_wizard(skip: bool = False) -> Embed:
@@ -158,7 +162,7 @@ async def send_poll(
max_value = use
msg = await ctx.send(embed=embed)
- select = MySelect(
+ select_obj = MySelect(
custom_id=str(msg.id),
placeholder=place,
max_values=max_value,
@@ -167,7 +171,7 @@ async def send_poll(
for index, option in enumerate(options)
],
)
- view_msg = await ctx.send(view=create_select_view(select=select, timeout=deadline))
+ view_msg = await ctx.send(view=create_select_view(select_obj=select_obj, timeout=deadline))
parsed_options: list[tuple[str, str]] = [
(obj.emoji, t.select.label(index + 1)) for index, obj in enumerate(options)
]
@@ -176,10 +180,8 @@ async def send_poll(
async def edit_poll_embed(embed: Embed, poll: Poll, missing: list[Member] = None) -> Embed:
- calc = get_percentage([]) # TODO: option votes
- print(calc)
+ calc = get_percentage(poll)
for index, field in enumerate(embed.fields):
- # TODO hier errechnen
if field.name == tg.status:
missing.sort(key=lambda m: str(m).lower())
*teamlers, last = (x.mention for x in missing)
@@ -189,6 +191,10 @@ async def edit_poll_embed(embed: Embed, poll: Poll, missing: list[Member] = None
name=field.name,
value=t.teamlers_missing(teamlers=", ".join(teamlers), last=last, cnt=len(teamlers) + 1),
)
+ else:
+ embed.set_field_at(
+ index, name=t.option.field.name(calc[index][0], calc[index][1]), value=field.value, inline=False
+ )
return embed
@@ -206,40 +212,43 @@ async def get_teamler(guild: Guild, team_roles: list[str]) -> set[Member]:
class MySelect(Select):
@db_wrapper
- async def callback(self, interaction):
+ async def callback(self, interaction): # TODO: Für den Fall, dass jemand das embed löscht muss noch was her
user = interaction.user
selected_options: list = self.values
message: Message = await interaction.channel.fetch_message(interaction.custom_id)
embed: Embed = message.embeds[0] if message.embeds else None
- poll: Poll = await db.get(Poll, message_id=message.id)
+ poll: Poll = await db.get(Poll, (Poll.options, Option.votes), message_id=message.id)
if not poll or not embed:
return
- options: list[Option] = await poll.get_options()
- options: list[Option] = [option for option in options if option.option in selected_options]
- missing: list[Member] = []
- print(missing)
+ options: list[Option] = poll.options
+ new_options: list[Option] = [option for option in options if option.option in selected_options]
+ missing: list[Member] | None = None
- old_selected: list[Voted] = await db.all(filter_by(Voted, user_id=user.id, poll_id=message.id))
- if old_selected:
- for old in old_selected:
- await old.remove()
+ opt: Option
+ for opt in poll.options:
+ for vote in opt.votes:
+ if vote.user_id == user.id:
+ await vote.remove()
+ opt.votes.remove(vote)
if poll.fair:
user_weight: float = await PollsDefaultSettings.everyone_power.get()
else:
user_weight: float = 1.0 # TODO: Add function to get user vote weight
- for option in options:
- await Voted.create(option_id=option.id, user_id=user.id, poll_id=poll.id, vote_weight=user_weight)
- print(await poll.get_voted_user())
+ for option in new_options:
+ option.votes.append(
+ await PollVote.create(option_id=option.id, user_id=user.id, poll_id=poll.id, vote_weight=user_weight)
+ )
if poll.poll_type == "team":
teamlers: set[Member] = await get_teamler(interaction.guild, ["team"])
- missing: list[Member] = []
+ missing: list[Member] | None = []
if user not in teamlers:
await interaction.response.send_message(content=t.team_yn_poll_forbidden, ephemeral=True)
return
- # await edit_poll_embed(embed, poll, missing)
+ embed = await edit_poll_embed(embed, poll, missing)
+ await message.edit(embed=embed)
class PollsCog(Cog, name="Polls"):
@@ -410,7 +419,6 @@ async def new(self, ctx: Context, *, options: str):
def check(m: Message):
return m.author == ctx.author
- print(options)
wizard = await ctx.send(embed=build_wizard())
mess: Message = await self.bot.wait_for("message", check=check, timeout=60.0)
args = mess.content
diff --git a/general/polls/models.py b/general/polls/models.py
index 69f4b191c..5dd677885 100644
--- a/general/polls/models.py
+++ b/general/polls/models.py
@@ -7,7 +7,7 @@
from sqlalchemy import BigInteger, Boolean, Column, Float, ForeignKey, Text
from sqlalchemy.orm import relationship
-from PyDrocsid.database import Base, UTCDateTime, db, filter_by, select
+from PyDrocsid.database import Base, UTCDateTime, db, select
class Poll(Base):
@@ -57,8 +57,10 @@ async def create(
fair=fair,
)
for position, poll_option in enumerate(options):
- await Option.create(
- poll=message_id, emote=poll_option[0], option_text=poll_option[1], field_position=position
+ row.options.append(
+ await Option.create(
+ poll=message_id, emote=poll_option[0], option_text=poll_option[1], field_position=position
+ )
)
await db.add(row)
@@ -67,19 +69,13 @@ async def create(
async def remove(self):
await db.delete(self)
- async def get_options(self) -> list[Option]:
- return list(await db.all(filter_by(Option, poll_id=self.message_id)))
-
- async def get_voted_user(self):
- return [await option.get_user() for option in await db.all(filter_by(Option, poll_id=self.message_id))]
-
class Option(Base):
__tablename__ = "poll_option"
id: Union[Column, int] = Column(BigInteger, primary_key=True, autoincrement=True, unique=True)
poll_id: Union[Column, int] = Column(BigInteger, ForeignKey("poll.message_id"))
- votes: list[Voted] = relationship("Voted", back_populates="option")
+ votes: list[PollVote] = relationship("PollVote", back_populates="option", cascade="all, delete")
poll: Poll = relationship("Poll", back_populates="options")
emote: Union[Column, str] = Column(Text(30))
option: Union[Column, str] = Column(Text(150))
@@ -89,27 +85,24 @@ class Option(Base):
async def create(poll: int, emote: str, option_text: str, field_position: int) -> Option:
options = Option(poll_id=poll, emote=emote, option=option_text, field_position=field_position)
await db.add(options)
-
return options
- async def get_user(self) -> list[Voted]:
- return list(await db.all(filter_by(Voted, option_id=self.id)))
-
-class Voted(Base):
+class PollVote(Base):
__tablename__ = "voted_user"
id: Union[Column, int] = Column(BigInteger, primary_key=True, autoincrement=True, unique=True)
user_id: Union[Column, int] = Column(BigInteger)
option_id: Union[Column, int] = Column(BigInteger, ForeignKey("poll_option.id"))
- option: Option = relationship("Option", back_populates="votes", cascade="all, delete")
+ option: Option = relationship("Option", back_populates="votes")
vote_weight: Union[Column, float] = Column(Float)
poll_id: Union[Column, int] = Column(BigInteger)
@staticmethod
async def create(user_id: int, option_id: int, vote_weight: float, poll_id: int):
- row = Voted(user_id=user_id, option_id=option_id, vote_weight=vote_weight, poll_id=poll_id)
+ row = PollVote(user_id=user_id, option_id=option_id, vote_weight=vote_weight, poll_id=poll_id)
await db.add(row)
+ return row
async def remove(self):
await db.delete(self)
@@ -127,7 +120,6 @@ class RoleWeight(Base):
async def create(role: int, weight: float) -> RoleWeight:
role_weight = RoleWeight(role_id=role, weight=weight, timestamp=utcnow())
await db.add(role_weight)
-
return role_weight
async def remove(self) -> None:
diff --git a/general/polls/translations/en.yml b/general/polls/translations/en.yml
index 60fda847f..27d8593c1 100644
--- a/general/polls/translations/en.yml
+++ b/general/polls/translations/en.yml
@@ -116,7 +116,7 @@ wizard:
--anonymous ANONYMOUS, -A ANONYMOUS
people can see who voted or not [Default: server settings]
--choices CHOICES, -C CHOICES
- the amount of votes someone can set
+ the amount of votes someone can set [Default: multiple choices]
```
example:
name: Example
From d29da5c470141b7cecbf608d6274369cdbb23fe4 Mon Sep 17 00:00:00 2001
From: NekoFanatic <83883849+NekoFanatic@users.noreply.github.com>
Date: Wed, 4 May 2022 20:37:23 +0200
Subject: [PATCH 18/44] some changes
---
general/polls/cog.py | 48 +++++++++++++++++++++++++++----
general/polls/translations/en.yml | 2 ++
2 files changed, 44 insertions(+), 6 deletions(-)
diff --git a/general/polls/cog.py b/general/polls/cog.py
index 651987ef8..84e7d50c3 100644
--- a/general/polls/cog.py
+++ b/general/polls/cog.py
@@ -140,8 +140,8 @@ async def send_poll(
embed = Embed(title=title, description=question, color=Colors.Polls, timestamp=utcnow())
embed.set_author(name=str(ctx.author), icon_url=ctx.author.display_avatar.url)
- end_time = calc_end_time(deadline)
- if end_time:
+ if deadline:
+ end_time = calc_end_time(deadline)
embed.set_footer(text=t.footer(end_time.strftime("%Y-%m-%d %H:%M:%S")))
if len(set(map(lambda x: x.emoji, options))) < len(options):
@@ -242,11 +242,18 @@ async def callback(self, interaction): # TODO: Für den Fall, dass jemand das e
)
if poll.poll_type == "team":
teamlers: set[Member] = await get_teamler(interaction.guild, ["team"])
- missing: list[Member] | None = []
if user not in teamlers:
await interaction.response.send_message(content=t.team_yn_poll_forbidden, ephemeral=True)
return
+ user_ids: set[int] = set()
+ for option in poll.options:
+ for vote in option.votes:
+ user_ids.add(vote.user_id)
+
+ missing: list[Member] | None = [teamler for teamler in teamlers if teamler.id not in user_ids]
+ missing.sort(key=lambda m: str(m).lower())
+
embed = await edit_poll_embed(embed, poll, missing)
await message.edit(embed=embed)
@@ -257,7 +264,7 @@ class PollsCog(Cog, name="Polls"):
Contributor.Defelo,
Contributor.TNT2k,
Contributor.wolflu,
- Contributor.NekoFanatic,
+ Contributor.NekoFanatic, # rewrote most of this code
]
def __init__(self, team_roles: list[str]):
@@ -270,6 +277,22 @@ async def poll(self, ctx: Context):
if not ctx.subcommand_passed:
raise UserInputError
+ @poll.command(name="delete", aliases=["del", "a"])
+ @docs(t.commands.poll.delete)
+ async def delete(self, ctx: Context, message: Message):
+ poll: Poll = await db.get(Poll, message_id=message.id)
+ if not poll:
+ raise CommandError(t.error.not_poll)
+ if not await PollsPermission.delete.check_permissions(ctx.author) and not poll.owner_id == ctx.author.id:
+ raise PermissionError
+
+ await message.delete()
+ interaction_message: Message = await ctx.channel.fetch_message(poll.interaction_message_id)
+ if interaction_message:
+ await interaction_message.delete()
+
+ await add_reactions(ctx.message, "white_check_mark")
+
@poll.group(name="settings", aliases=["s"])
@PollsPermission.read.check
@docs(t.commands.poll.settings.settings)
@@ -442,7 +465,7 @@ def check(m: Message):
if isinstance(deadline, int):
deadline: int = deadline
else:
- deadline = await PollsDefaultSettings.duration.get() # TODO implement parsing for datetimes
+ deadline = await PollsDefaultSettings.duration.get()
anonymous: bool = parsed.anonymous
choices: int = parsed.choices
@@ -511,6 +534,19 @@ async def team_yesno(self, ctx: Context, *, text: str):
teamlers: list[str]
field = (tg.status, t.teamlers_missing(teamlers=", ".join(teamlers), last=last, cnt=len(teamlers) + 1))
- embed_msg, interaction, parsed_options, question = await send_poll(
+ message, interaction, parsed_options, question = await send_poll(
ctx=ctx, title=t.team_poll, max_choices=1, poll_args=options, field=field
)
+ await Poll.create(
+ message_id=message.id,
+ channel=message.channel.id,
+ owner=ctx.author.id,
+ title=question,
+ end=None,
+ anonymous=False,
+ can_delete=False,
+ options=parsed_options,
+ poll_type="team",
+ interaction=interaction.id,
+ fair=True,
+ )
diff --git a/general/polls/translations/en.yml b/general/polls/translations/en.yml
index 27d8593c1..d5bcfe0bd 100644
--- a/general/polls/translations/en.yml
+++ b/general/polls/translations/en.yml
@@ -3,6 +3,7 @@ commands:
poll: poll commands
quick: small poll with default options
new: advanced poll with more options
+ delete: delete polls
settings:
settings: poll settings
roles_weights: manage weight for certain roles
@@ -23,6 +24,7 @@ permissions:
error:
weight_too_small: "Weight cant be lower than `0.1`"
cant_set_weight: Can't set weight!
+ not_poll: Mesage doesn't contains a poll
poll_config:
title: Default poll configuration
From 9e9cbe6281b59f5cd6a95d9847b21da122416f7e Mon Sep 17 00:00:00 2001
From: NekoFanatic <83883849+NekoFanatic@users.noreply.github.com>
Date: Wed, 4 May 2022 21:19:15 +0200
Subject: [PATCH 19/44] added command to show who voted what
---
general/polls/cog.py | 30 ++++++++++++++++++++++++++++++
general/polls/translations/en.yml | 5 +++++
2 files changed, 35 insertions(+)
diff --git a/general/polls/cog.py b/general/polls/cog.py
index 84e7d50c3..8abc5661d 100644
--- a/general/polls/cog.py
+++ b/general/polls/cog.py
@@ -287,12 +287,42 @@ async def delete(self, ctx: Context, message: Message):
raise PermissionError
await message.delete()
+ await poll.remove()
interaction_message: Message = await ctx.channel.fetch_message(poll.interaction_message_id)
if interaction_message:
await interaction_message.delete()
await add_reactions(ctx.message, "white_check_mark")
+ @poll.command(name="voted", aliases=["v"])
+ @docs(t.commands.poll.voted)
+ async def voted(self, ctx: Context, message: Message):
+ poll: Poll = await db.get(Poll, (Poll.options, Option.votes), message_id=message.id)
+ author = ctx.author
+ if not poll:
+ raise CommandError(t.error.not_poll)
+ if (
+ poll.anonymous
+ and not await PollsPermission.anonymous_bypass.check_permissions(author)
+ and not poll.owner_id == author.id
+ ):
+ raise PermissionError
+
+ users: dict[str, list[int]] = {}
+ for option in poll.options:
+ for vote in option.votes:
+ try:
+ users[str(vote.user_id)].append(option.field_position + 1)
+ except KeyError:
+ users[str(vote.user_id)] = [option.field_position + 1]
+
+ description = ""
+ for key, value in users.items():
+ description += t.voted.row(key, value)
+ embed = Embed(title=t.voted.title, description=description, color=Colors.Polls)
+
+ await send_long_embed(ctx, embed=embed, repeat_title=True, paginate=True)
+
@poll.group(name="settings", aliases=["s"])
@PollsPermission.read.check
@docs(t.commands.poll.settings.settings)
diff --git a/general/polls/translations/en.yml b/general/polls/translations/en.yml
index d5bcfe0bd..203a05b58 100644
--- a/general/polls/translations/en.yml
+++ b/general/polls/translations/en.yml
@@ -4,6 +4,7 @@ commands:
quick: small poll with default options
new: advanced poll with more options
delete: delete polls
+ voted: show who voted on the poll (only works if not anonymous or if the poll-owner uses the command)
settings:
settings: poll settings
roles_weights: manage weight for certain roles
@@ -67,6 +68,10 @@ votes:
many: "Set default votes for a poll to {cnt} votes"
reset: "Set the default votes for polls to unlimited"
+voted:
+ title: Votes
+ row: "\n <@{}> -> Options: {}"
+
anonymous:
is_on: made default poll votes anonymous
is_off: made default poll votes visible
From 0a391fc852d6d500ec7710b6ad8621b862c97138 Mon Sep 17 00:00:00 2001
From: NekoFanatic <83883849+NekoFanatic@users.noreply.github.com>
Date: Fri, 6 May 2022 19:35:40 +0200
Subject: [PATCH 20/44] Some more changes
---
general/polls/cog.py | 107 ++++++++++++++++++++++++------
general/polls/models.py | 2 +
general/polls/settings.py | 3 +-
general/polls/translations/en.yml | 17 ++++-
4 files changed, 107 insertions(+), 22 deletions(-)
diff --git a/general/polls/cog.py b/general/polls/cog.py
index 8abc5661d..0556f2f6e 100644
--- a/general/polls/cog.py
+++ b/general/polls/cog.py
@@ -5,15 +5,15 @@
from typing import Optional, Union
from dateutil.relativedelta import relativedelta
-from discord import Embed, Forbidden, Guild, Member, Message, Role, SelectOption
-from discord.ext import commands
+from discord import Embed, Forbidden, Guild, HTTPException, Member, Message, Role, SelectOption
+from discord.ext import commands, tasks
from discord.ext.commands import CommandError, Context, UserInputError, guild_only
from discord.ui import Select, View
from discord.utils import utcnow
from PyDrocsid.cog import Cog
from PyDrocsid.command import add_reactions, docs
-from PyDrocsid.database import db, db_wrapper
+from PyDrocsid.database import db, db_wrapper, filter_by
from PyDrocsid.embeds import EmbedLimits, send_long_embed
from PyDrocsid.emojis import emoji_to_name, name_to_emoji
from PyDrocsid.settings import RoleSettings
@@ -25,7 +25,7 @@
from .permissions import PollsPermission
from .settings import PollsDefaultSettings
from ...contributor import Contributor
-from ...pubsub import send_to_changelog
+from ...pubsub import send_alert, send_to_changelog
tg = t.g
@@ -46,7 +46,7 @@ def __init__(self, ctx: Context, line: str, number: int):
custom_emoji_match = re.fullmatch(r"", emoji_candidate)
if custom_emoji := ctx.bot.get_emoji(int(custom_emoji_match.group(1))) if custom_emoji_match else None:
- self.emoji = custom_emoji
+ self.emoji = str(custom_emoji)
self.option = text.strip()
elif (unicode_emoji := emoji_candidate) in emoji_to_name:
self.emoji = unicode_emoji
@@ -78,7 +78,7 @@ def get_percentage(poll: Poll) -> list[tuple[float, float]]:
for option in options:
values.append(sum([vote.vote_weight for vote in option.votes]))
- return [(value, round(((value / sum(values)) * 100), 2)) for value in values]
+ return [(float(value), float(round(((value / sum(values)) * 100), 2))) for value in values]
def build_wizard(skip: bool = False) -> Embed:
@@ -107,7 +107,7 @@ async def get_parser() -> ArgumentParser:
def calc_end_time(duration: Optional[float]) -> Optional[datetime]:
if duration != 0 and not None:
- return datetime.today() + relativedelta(hours=int(duration))
+ return utcnow() + relativedelta(hours=int(duration))
return
@@ -142,13 +142,13 @@ async def send_poll(
if deadline:
end_time = calc_end_time(deadline)
- embed.set_footer(text=t.footer(end_time.strftime("%Y-%m-%d %H:%M:%S")))
+ embed.set_footer(text=t.footer(end_time.strftime("%Y-%m-%d %H:%M")))
if len(set(map(lambda x: x.emoji, options))) < len(options):
raise CommandError(t.option_duplicated)
for option in options:
- embed.add_field(name=t.option.field.name(0, 0.0), value=str(option), inline=False)
+ embed.add_field(name=t.option.field.name(0, 0), value=str(option), inline=False)
if field:
embed.add_field(name=field[0], value=field[1], inline=False)
@@ -171,11 +171,19 @@ async def send_poll(
for index, option in enumerate(options)
],
)
- view_msg = await ctx.send(view=create_select_view(select_obj=select_obj, timeout=deadline))
+ view_msg = await ctx.send(view=create_select_view(select_obj=select_obj))
parsed_options: list[tuple[str, str]] = [
(obj.emoji, t.select.label(index + 1)) for index, obj in enumerate(options)
]
-
+ try:
+ await msg.pin()
+ except HTTPException:
+ embed = Embed(
+ title=t.error.cant_pin.title,
+ description=t.error.cant_pin.description(ctx.channel.mention),
+ color=Colors.error,
+ )
+ await send_alert(ctx.guild, embed)
return msg, view_msg, parsed_options, question
@@ -192,9 +200,9 @@ async def edit_poll_embed(embed: Embed, poll: Poll, missing: list[Member] = None
value=t.teamlers_missing(teamlers=", ".join(teamlers), last=last, cnt=len(teamlers) + 1),
)
else:
- embed.set_field_at(
- index, name=t.option.field.name(calc[index][0], calc[index][1]), value=field.value, inline=False
- )
+ weight: float | int = calc[index][0] if not calc[index][0].is_integer() else int(calc[index][0])
+ percentage: float | int = calc[index][1] if not calc[index][1].is_integer() else int(calc[index][1])
+ embed.set_field_at(index, name=t.option.field.name(weight, percentage), value=field.value, inline=False)
return embed
@@ -212,10 +220,12 @@ async def get_teamler(guild: Guild, team_roles: list[str]) -> set[Member]:
class MySelect(Select):
@db_wrapper
- async def callback(self, interaction): # TODO: Für den Fall, dass jemand das embed löscht muss noch was her
+ async def callback(self, interaction):
user = interaction.user
selected_options: list = self.values
- message: Message = await interaction.channel.fetch_message(interaction.custom_id)
+ message: Message = await interaction.channel.fetch_message(
+ interaction.custom_id
+ ) # TODO: Für den Fall, dass jemand das embed löscht muss noch was her
embed: Embed = message.embeds[0] if message.embeds else None
poll: Poll = await db.get(Poll, (Poll.options, Option.votes), message_id=message.id)
if not poll or not embed:
@@ -270,6 +280,32 @@ class PollsCog(Cog, name="Polls"):
def __init__(self, team_roles: list[str]):
self.team_roles: list[str] = team_roles
+ async def on_ready(self):
+ try:
+ self.poll_loop.start()
+ except RuntimeError:
+ self.poll_loop.restart()
+
+ @tasks.loop(minutes=1)
+ @db_wrapper
+ async def poll_loop(self):
+ polls: list[Poll] = await db.all(filter_by(Poll, active=True))
+
+ for poll in polls:
+ if poll.end_time < utcnow():
+ channel = await self.bot.fetch_channel(poll.channel_id)
+ embed_message = await channel.fetch_message(poll.message_id)
+ interaction_message = await channel.fetch_message(poll.interaction_message_id)
+
+ await interaction_message.delete()
+ embed = embed_message.embeds[0]
+ embed.set_footer(text=t.footer_closed)
+
+ await embed_message.edit(embed=embed)
+ await embed_message.unpin()
+
+ poll.active = False
+
@commands.group(name="poll", aliases=["vote"])
@guild_only()
@docs(t.commands.poll.poll)
@@ -339,6 +375,11 @@ async def settings(self, ctx: Context):
value=t.poll_config.duration.time(cnt=time) if not time <= 0 else t.poll_config.duration.unlimited,
inline=False,
)
+ embed.add_field(
+ name=t.poll_config.max_duration.name,
+ value=t.poll_config.max_duration.time(cnt=time) if not time <= 0 else t.poll_config.max_duration.unlimited,
+ inline=False,
+ )
choice: int = await PollsDefaultSettings.max_choices.get()
embed.add_field(
name=t.poll_config.choices.name,
@@ -395,6 +436,18 @@ async def duration(self, ctx: Context, hours: int = None):
await add_reactions(ctx.message, "white_check_mark")
await send_to_changelog(ctx.guild, msg)
+ @settings.command(name="max_duration", aliases=["md"])
+ @PollsPermission.write.check
+ @docs(t.commands.poll.settings.max_duration)
+ async def max_duration(self, ctx: Context, days: int = None):
+ if not days:
+ days = 7
+ msg: str = t.max_duration.set(cnt=days)
+
+ await PollsDefaultSettings.max_duration.set(days)
+ await add_reactions(ctx.message, "white_check_mark")
+ await send_to_changelog(ctx.guild, msg)
+
@settings.command(name="votes", aliases=["v", "choices", "c"])
@PollsPermission.write.check
@docs(t.commands.poll.settings.votes)
@@ -446,6 +499,8 @@ async def everyone(self, ctx: Context, weight: float = None):
@docs(t.commands.poll.quick)
async def quick(self, ctx: Context, *, args: str):
deadline = await PollsDefaultSettings.duration.get()
+ if deadline == 0:
+ deadline = await PollsDefaultSettings.max_duration.get()
max_choices = await PollsDefaultSettings.max_choices.get()
anonymous = await PollsDefaultSettings.anonymous.get()
message, interaction, parsed_options, question = await send_poll(
@@ -493,9 +548,16 @@ def check(m: Message):
title: str = t.poll
deadline: Union[list[str, str], int] = parsed.deadline
if isinstance(deadline, int):
- deadline: int = deadline
+ deadline = (
+ deadline
+ if deadline <= await PollsDefaultSettings.max_duration.get()
+ else PollsDefaultSettings.max_duration.get()
+ )
else:
- deadline = await PollsDefaultSettings.duration.get()
+ if await PollsDefaultSettings.duration.get() == 0:
+ deadline = await PollsDefaultSettings.max_duration.get()
+ else:
+ deadline = await PollsDefaultSettings.duration.get()
anonymous: bool = parsed.anonymous
choices: int = parsed.choices
@@ -565,14 +627,19 @@ async def team_yesno(self, ctx: Context, *, text: str):
field = (tg.status, t.teamlers_missing(teamlers=", ".join(teamlers), last=last, cnt=len(teamlers) + 1))
message, interaction, parsed_options, question = await send_poll(
- ctx=ctx, title=t.team_poll, max_choices=1, poll_args=options, field=field
+ ctx=ctx,
+ title=t.team_poll,
+ max_choices=1,
+ poll_args=options,
+ field=field,
+ deadline=await PollsDefaultSettings.max_duration.get() * 24,
)
await Poll.create(
message_id=message.id,
channel=message.channel.id,
owner=ctx.author.id,
title=question,
- end=None,
+ end=calc_end_time(await PollsDefaultSettings.max_duration.get() * 24),
anonymous=False,
can_delete=False,
options=parsed_options,
diff --git a/general/polls/models.py b/general/polls/models.py
index 5dd677885..63728d409 100644
--- a/general/polls/models.py
+++ b/general/polls/models.py
@@ -28,6 +28,7 @@ class Poll(Base):
anonymous: Union[Column, bool] = Column(Boolean)
can_delete: Union[Column, bool] = Column(Boolean)
fair: Union[Column, bool] = Column(Boolean)
+ active: Union[Column, bool] = Column(Boolean)
@staticmethod
async def create(
@@ -55,6 +56,7 @@ async def create(
can_delete=can_delete,
interaction_message_id=interaction,
fair=fair,
+ active=True,
)
for position, poll_option in enumerate(options):
row.options.append(
diff --git a/general/polls/settings.py b/general/polls/settings.py
index 775fe0f14..ad6321dee 100644
--- a/general/polls/settings.py
+++ b/general/polls/settings.py
@@ -2,7 +2,8 @@
class PollsDefaultSettings(Settings):
- duration = 0 # 0 for unlimited duration (duration in hours)
+ duration = 0 # 0 for max_duration duration (duration in hours)
+ max_duration = 7 # max duration (duration in days)
max_choices = 0 # 0 for unlimited choices
type = "standard"
everyone_power = 1.0
diff --git a/general/polls/translations/en.yml b/general/polls/translations/en.yml
index 203a05b58..8989736cc 100644
--- a/general/polls/translations/en.yml
+++ b/general/polls/translations/en.yml
@@ -9,6 +9,7 @@ commands:
settings: poll settings
roles_weights: manage weight for certain roles
duration: set the default hours a poll should be open
+ max_duration: set the maximum duration a poll can be opened
votes: set the default amount of votes a user can have on polls
anonymous: set if user can see who voted on a poll
everyone: manage role weight for the default role
@@ -26,6 +27,9 @@ error:
weight_too_small: "Weight cant be lower than `0.1`"
cant_set_weight: Can't set weight!
not_poll: Mesage doesn't contains a poll
+ cant_pin:
+ title: Error
+ description: Can't pin any more messages in {}
poll_config:
title: Default poll configuration
@@ -34,7 +38,12 @@ poll_config:
time:
one: "{cnt} hour"
many: "{cnt} hours"
- unlimited: unlimited
+ unlimited: max duration
+ max_duration:
+ name: "**Max Duration**"
+ time:
+ one: "{cnt} days"
+ many: "{cnt} days"
choices:
name: "**Choices per user**"
amount:
@@ -62,6 +71,11 @@ duration:
many: "Set default duration for poll to {cnt} hours"
reset: "Set the default duration for polls to unlimited"
+max_duration:
+ set:
+ one: "Set maximum duration for a poll to {cnt} day"
+ many: "Set maximum duration for a poll to {cnt} days"
+
votes:
set:
one: "Set default votes for a poll to {cnt} vote"
@@ -148,6 +162,7 @@ teamlers_missing:
one: "{last} hasn't voted yet."
many: "{teamlers} and {last} haven't voted yet."
footer: Ends at `{}`
+footer_closed: Closed
can_not_use_wastebucket_as_option: "You can not use :wastebasket: as option"
foreign_message: "You are not allowed to add yes/no reactions to foreign messages!"
could_not_add_reactions: Could not add reactions because I don't have `add_reactions` permission in {}.
From 9bdcbe30777529df3fbb7136ea361b329e07a4cc Mon Sep 17 00:00:00 2001
From: NekoFanatic <83883849+NekoFanatic@users.noreply.github.com>
Date: Sat, 7 May 2022 16:52:20 +0200
Subject: [PATCH 21/44] Some more changes
---
general/polls/cog.py | 39 +++++++++++++++++++++++++++++++++------
general/polls/models.py | 14 +++++++++-----
2 files changed, 42 insertions(+), 11 deletions(-)
diff --git a/general/polls/cog.py b/general/polls/cog.py
index 0556f2f6e..69f6977cb 100644
--- a/general/polls/cog.py
+++ b/general/polls/cog.py
@@ -5,7 +5,7 @@
from typing import Optional, Union
from dateutil.relativedelta import relativedelta
-from discord import Embed, Forbidden, Guild, HTTPException, Member, Message, Role, SelectOption
+from discord import Embed, Forbidden, Guild, HTTPException, Member, Message, NotFound, Role, SelectOption
from discord.ext import commands, tasks
from discord.ext.commands import CommandError, Context, UserInputError, guild_only
from discord.ui import Select, View
@@ -223,9 +223,7 @@ class MySelect(Select):
async def callback(self, interaction):
user = interaction.user
selected_options: list = self.values
- message: Message = await interaction.channel.fetch_message(
- interaction.custom_id
- ) # TODO: Für den Fall, dass jemand das embed löscht muss noch was her
+ message: Message = await interaction.channel.fetch_message(interaction.custom_id)
embed: Embed = message.embeds[0] if message.embeds else None
poll: Poll = await db.get(Poll, (Poll.options, Option.votes), message_id=message.id)
if not poll or not embed:
@@ -286,6 +284,30 @@ async def on_ready(self):
except RuntimeError:
self.poll_loop.restart()
+ async def on_message_delete(self, message: Message):
+ deleted_embed: Poll | None = await db.get(Poll, message_id=message.id)
+ deleted_interaction: Poll | None = await db.get(Poll, interaction_message_id=message.id)
+
+ if not deleted_embed and not deleted_interaction:
+ return
+
+ poll = deleted_embed or deleted_interaction
+ channel = await self.bot.fetch_channel(poll.channel_id)
+ try:
+ if deleted_interaction:
+ msg: Message | None = await channel.fetch_message(poll.message_id)
+ else:
+ msg: Message | None = await channel.fetch_message(poll.interaction_message_id)
+ except NotFound:
+ msg = None
+
+ await poll.remove()
+ if msg:
+ try:
+ await msg.delete()
+ except NotFound:
+ pass
+
@tasks.loop(minutes=1)
@db_wrapper
async def poll_loop(self):
@@ -313,6 +335,8 @@ async def poll(self, ctx: Context):
if not ctx.subcommand_passed:
raise UserInputError
+ # TODO: list of all active polls
+
@poll.command(name="delete", aliases=["del", "a"])
@docs(t.commands.poll.delete)
async def delete(self, ctx: Context, message: Message):
@@ -388,7 +412,7 @@ async def settings(self, ctx: Context):
)
anonymous: bool = await PollsDefaultSettings.anonymous.get()
embed.add_field(name=t.poll_config.anonymous.name, value=str(anonymous), inline=False)
- roles = await RoleWeight.get()
+ roles = await RoleWeight.get(ctx.guild.id)
everyone: int = await PollsDefaultSettings.everyone_power.get()
base: str = t.poll_config.roles.ev_row(ctx.guild.default_role, everyone)
if roles:
@@ -413,7 +437,7 @@ async def roles_weights(self, ctx: Context, role: Role, weight: float = None):
element.weight = weight
msg: str = t.role_weight.set(role.id, weight)
elif weight and not element:
- await RoleWeight.create(role.id, weight)
+ await RoleWeight.create(ctx.guild.id, role.id, weight)
msg: str = t.role_weight.set(role.id, weight)
else:
await element.remove()
@@ -509,6 +533,7 @@ async def quick(self, ctx: Context, *, args: str):
await Poll.create(
message_id=message.id,
+ guild_id=ctx.guild.id,
channel=message.channel.id,
owner=ctx.author.id,
title=question,
@@ -579,6 +604,7 @@ def check(m: Message):
await Poll.create(
message_id=message.id,
+ guild_id=ctx.guild.id,
channel=message.channel.id,
owner=ctx.author.id,
title=question,
@@ -636,6 +662,7 @@ async def team_yesno(self, ctx: Context, *, text: str):
)
await Poll.create(
message_id=message.id,
+ guild_id=ctx.guild.id,
channel=message.channel.id,
owner=ctx.author.id,
title=question,
diff --git a/general/polls/models.py b/general/polls/models.py
index 63728d409..57d06a6e2 100644
--- a/general/polls/models.py
+++ b/general/polls/models.py
@@ -7,7 +7,7 @@
from sqlalchemy import BigInteger, Boolean, Column, Float, ForeignKey, Text
from sqlalchemy.orm import relationship
-from PyDrocsid.database import Base, UTCDateTime, db, select
+from PyDrocsid.database import Base, UTCDateTime, db, filter_by
class Poll(Base):
@@ -18,6 +18,7 @@ class Poll(Base):
options: list[Option] = relationship("Option", back_populates="poll", cascade="all, delete")
message_id: Union[Column, int] = Column(BigInteger, unique=True)
+ guild_id: Union[Column, int] = Column(BigInteger)
interaction_message_id: Union[Column, int] = Column(BigInteger, unique=True)
channel_id: Union[Column, int] = Column(BigInteger)
owner_id: Union[Column, int] = Column(BigInteger)
@@ -33,6 +34,7 @@ class Poll(Base):
@staticmethod
async def create(
message_id: int,
+ guild_id: int,
channel: int,
owner: int,
title: str,
@@ -46,6 +48,7 @@ async def create(
) -> Poll:
row = Poll(
message_id=message_id,
+ guild_id=guild_id,
channel_id=channel,
owner_id=owner,
timestamp=utcnow(),
@@ -114,13 +117,14 @@ class RoleWeight(Base):
__tablename__ = "role_weight"
id: Union[Column, int] = Column(BigInteger, primary_key=True, autoincrement=True, unique=True)
+ guild_id: Union[Column, int] = Column(BigInteger)
role_id: Union[Column, int] = Column(BigInteger, unique=True)
weight: Union[Column, float] = Column(Float)
timestamp: Union[Column, datetime] = Column(UTCDateTime)
@staticmethod
- async def create(role: int, weight: float) -> RoleWeight:
- role_weight = RoleWeight(role_id=role, weight=weight, timestamp=utcnow())
+ async def create(guild_id: int, role: int, weight: float) -> RoleWeight:
+ role_weight = RoleWeight(guild_id=guild_id, role_id=role, weight=weight, timestamp=utcnow())
await db.add(role_weight)
return role_weight
@@ -128,5 +132,5 @@ async def remove(self) -> None:
await db.delete(self)
@staticmethod
- async def get() -> list[RoleWeight]:
- return await db.all(select(RoleWeight))
+ async def get(guild: int) -> list[RoleWeight]:
+ return await db.all(filter_by(RoleWeight, guild_id=guild))
From 70ff070043971517ee7e0fdb5c01c63059abe82f Mon Sep 17 00:00:00 2001
From: NekoFanatic <83883849+NekoFanatic@users.noreply.github.com>
Date: Sat, 7 May 2022 20:24:30 +0200
Subject: [PATCH 22/44] some more changes
---
general/polls/cog.py | 130 +++++++++++++++++++-----------
general/polls/models.py | 3 +
general/polls/settings.py | 1 +
general/polls/translations/en.yml | 20 ++++-
4 files changed, 107 insertions(+), 47 deletions(-)
diff --git a/general/polls/cog.py b/general/polls/cog.py
index 69f6977cb..765c9ecf3 100644
--- a/general/polls/cog.py
+++ b/general/polls/cog.py
@@ -5,7 +5,18 @@
from typing import Optional, Union
from dateutil.relativedelta import relativedelta
-from discord import Embed, Forbidden, Guild, HTTPException, Member, Message, NotFound, Role, SelectOption
+from discord import (
+ Embed,
+ Forbidden,
+ Guild,
+ HTTPException,
+ Member,
+ Message,
+ NotFound,
+ RawMessageDeleteEvent,
+ Role,
+ SelectOption,
+)
from discord.ext import commands, tasks
from discord.ext.commands import CommandError, Context, UserInputError, guild_only
from discord.ui import Select, View
@@ -101,6 +112,7 @@ async def get_parser() -> ArgumentParser:
"--anonymous", "-A", default=await PollsDefaultSettings.anonymous.get(), type=bool, choices=[True, False]
)
parser.add_argument("--choices", "-C", default=await PollsDefaultSettings.max_choices.get(), type=int)
+ parser.add_argument("--fair", "-F", default=await PollsDefaultSettings.fair.get(), type=bool, choices=[True, False])
return parser
@@ -266,6 +278,28 @@ async def callback(self, interaction):
await message.edit(embed=embed)
+async def handle_deleted_messages(bot, message_id: int):
+ deleted_embed: Poll | None = await db.get(Poll, message_id=message_id)
+ deleted_interaction: Poll | None = await db.get(Poll, interaction_message_id=message_id)
+
+ if not deleted_embed and not deleted_interaction:
+ return
+
+ poll = deleted_embed or deleted_interaction
+ channel = await bot.fetch_channel(poll.channel_id)
+ try:
+ if deleted_interaction:
+ msg: Message | None = await channel.fetch_message(poll.message_id)
+ else:
+ msg: Message | None = await channel.fetch_message(poll.interaction_message_id)
+ except NotFound:
+ msg = None
+
+ if msg:
+ await poll.remove()
+ await msg.delete()
+
+
class PollsCog(Cog, name="Polls"):
CONTRIBUTORS = [
Contributor.MaxiHuHe04,
@@ -285,28 +319,10 @@ async def on_ready(self):
self.poll_loop.restart()
async def on_message_delete(self, message: Message):
- deleted_embed: Poll | None = await db.get(Poll, message_id=message.id)
- deleted_interaction: Poll | None = await db.get(Poll, interaction_message_id=message.id)
-
- if not deleted_embed and not deleted_interaction:
- return
+ await handle_deleted_messages(self.bot, message.id)
- poll = deleted_embed or deleted_interaction
- channel = await self.bot.fetch_channel(poll.channel_id)
- try:
- if deleted_interaction:
- msg: Message | None = await channel.fetch_message(poll.message_id)
- else:
- msg: Message | None = await channel.fetch_message(poll.interaction_message_id)
- except NotFound:
- msg = None
-
- await poll.remove()
- if msg:
- try:
- await msg.delete()
- except NotFound:
- pass
+ async def on_raw_message_delete(self, event: RawMessageDeleteEvent):
+ await handle_deleted_messages(self.bot, event.message_id)
@tasks.loop(minutes=1)
@db_wrapper
@@ -314,6 +330,9 @@ async def poll_loop(self):
polls: list[Poll] = await db.all(filter_by(Poll, active=True))
for poll in polls:
+ if not poll.end_time:
+ await poll.remove()
+ continue
if poll.end_time < utcnow():
channel = await self.bot.fetch_channel(poll.channel_id)
embed_message = await channel.fetch_message(poll.message_id)
@@ -335,9 +354,30 @@ async def poll(self, ctx: Context):
if not ctx.subcommand_passed:
raise UserInputError
- # TODO: list of all active polls
-
- @poll.command(name="delete", aliases=["del", "a"])
+ @poll.command(name="list", aliases=["l"])
+ @guild_only()
+ @docs(t.commands.poll.list)
+ async def list(self, ctx: Context):
+ polls: list[Poll] = await db.all(filter_by(Poll, active=True, guild_id=ctx.guild.id))
+ if polls:
+ description = ""
+ for poll in polls:
+ if poll.poll_type == "team" and not await PollsPermission.team_poll.check_permissions(ctx.author):
+ continue
+ if poll.poll_type == "team":
+ description += t.polls.team_row(
+ poll.title, poll.message_url, poll.owner_id, poll.end_time.strftime("%Y-%m-%d %H:%M")
+ )
+ else:
+ description += t.polls.row(
+ poll.title, poll.message_url, poll.owner_id, poll.end_time.strftime("%Y-%m-%d %H:%M")
+ )
+
+ embed: Embed = Embed(title=t.polls.title, description=description, color=Colors.Polls)
+
+ await send_long_embed(ctx, embed=embed, paginate=True)
+
+ @poll.command(name="delete", aliases=["del"])
@docs(t.commands.poll.delete)
async def delete(self, ctx: Context, message: Message):
poll: Poll = await db.get(Poll, message_id=message.id)
@@ -394,15 +434,14 @@ async def settings(self, ctx: Context):
embed = Embed(title=t.poll_config.title, color=Colors.Polls)
time: int = await PollsDefaultSettings.duration.get()
+ max_time: int = await PollsDefaultSettings.max_duration.get()
embed.add_field(
name=t.poll_config.duration.name,
- value=t.poll_config.duration.time(cnt=time) if not time <= 0 else t.poll_config.duration.unlimited,
+ value=t.poll_config.duration.time(cnt=time) if not time <= 0 else t.poll_config.duration.time(cnt=max_time),
inline=False,
)
embed.add_field(
- name=t.poll_config.max_duration.name,
- value=t.poll_config.max_duration.time(cnt=time) if not time <= 0 else t.poll_config.max_duration.unlimited,
- inline=False,
+ name=t.poll_config.max_duration.name, value=t.poll_config.max_duration.time(cnt=max_time), inline=False
)
choice: int = await PollsDefaultSettings.max_choices.get()
embed.add_field(
@@ -502,20 +541,16 @@ async def anonymous(self, ctx: Context, status: bool):
await add_reactions(ctx.message, "white_check_mark")
await send_to_changelog(ctx.guild, msg)
- @settings.command(name="everyone", aliases=["e"])
+ @settings.command(name="fair", aliases=["f"])
@PollsPermission.write.check
- @docs(t.commands.poll.settings.everyone)
- async def everyone(self, ctx: Context, weight: float = None):
- if weight and weight < 0.1:
- raise CommandError(t.error.weight_too_small)
-
- if not weight:
- await PollsDefaultSettings.everyone_power.set(1.0)
- msg: str = t.weight_everyone.reset
+ @docs(t.commands.poll.settings.fair)
+ async def fair(self, ctx: Context, status: bool):
+ if status:
+ msg: str = t.fair.is_on
else:
- await PollsDefaultSettings.everyone_power.set(weight)
- msg: str = t.weight_everyone.set(cnt=weight)
+ msg: str = t.fair.is_off
+ await PollsDefaultSettings.fair.set(status)
await add_reactions(ctx.message, "white_check_mark")
await send_to_changelog(ctx.guild, msg)
@@ -533,6 +568,7 @@ async def quick(self, ctx: Context, *, args: str):
await Poll.create(
message_id=message.id,
+ message_url=message.jump_url,
guild_id=ctx.guild.id,
channel=message.channel.id,
owner=ctx.author.id,
@@ -541,11 +577,13 @@ async def quick(self, ctx: Context, *, args: str):
anonymous=anonymous,
can_delete=True,
options=parsed_options,
- poll_type="standard",
+ poll_type=await PollsDefaultSettings.type.get(),
interaction=interaction.id,
- fair=False,
+ fair=await PollsDefaultSettings.fair.get(),
)
+ await ctx.message.delete()
+
@poll.command(name="new", usage=t.usage.poll)
@docs(t.commands.poll.new)
async def new(self, ctx: Context, *, options: str):
@@ -575,8 +613,8 @@ def check(m: Message):
if isinstance(deadline, int):
deadline = (
deadline
- if deadline <= await PollsDefaultSettings.max_duration.get()
- else PollsDefaultSettings.max_duration.get()
+ if deadline >= await PollsDefaultSettings.max_duration.get()
+ else await PollsDefaultSettings.max_duration.get()
)
else:
if await PollsDefaultSettings.duration.get() == 0:
@@ -594,7 +632,7 @@ def check(m: Message):
teamlers: list[str]
field = (tg.status, t.teamlers_missing(teamlers=", ".join(teamlers), last=last, cnt=len(teamlers) + 1))
else:
- can_delete, fair = True, False
+ can_delete, fair = True, parsed.fair
field = None
message, interaction, parsed_options, question = await send_poll(
@@ -604,6 +642,7 @@ def check(m: Message):
await Poll.create(
message_id=message.id,
+ message_url=message.jump_url,
guild_id=ctx.guild.id,
channel=message.channel.id,
owner=ctx.author.id,
@@ -662,6 +701,7 @@ async def team_yesno(self, ctx: Context, *, text: str):
)
await Poll.create(
message_id=message.id,
+ message_url=message.jump_url,
guild_id=ctx.guild.id,
channel=message.channel.id,
owner=ctx.author.id,
diff --git a/general/polls/models.py b/general/polls/models.py
index 57d06a6e2..36875c890 100644
--- a/general/polls/models.py
+++ b/general/polls/models.py
@@ -18,6 +18,7 @@ class Poll(Base):
options: list[Option] = relationship("Option", back_populates="poll", cascade="all, delete")
message_id: Union[Column, int] = Column(BigInteger, unique=True)
+ message_url: Union[Column, str] = Column(Text(256))
guild_id: Union[Column, int] = Column(BigInteger)
interaction_message_id: Union[Column, int] = Column(BigInteger, unique=True)
channel_id: Union[Column, int] = Column(BigInteger)
@@ -34,6 +35,7 @@ class Poll(Base):
@staticmethod
async def create(
message_id: int,
+ message_url: str,
guild_id: int,
channel: int,
owner: int,
@@ -48,6 +50,7 @@ async def create(
) -> Poll:
row = Poll(
message_id=message_id,
+ message_url=message_url,
guild_id=guild_id,
channel_id=channel,
owner_id=owner,
diff --git a/general/polls/settings.py b/general/polls/settings.py
index ad6321dee..90e552cd1 100644
--- a/general/polls/settings.py
+++ b/general/polls/settings.py
@@ -8,3 +8,4 @@ class PollsDefaultSettings(Settings):
type = "standard"
everyone_power = 1.0
anonymous = False
+ fair = False
diff --git a/general/polls/translations/en.yml b/general/polls/translations/en.yml
index 8989736cc..6655eae22 100644
--- a/general/polls/translations/en.yml
+++ b/general/polls/translations/en.yml
@@ -5,6 +5,7 @@ commands:
new: advanced poll with more options
delete: delete polls
voted: show who voted on the poll (only works if not anonymous or if the poll-owner uses the command)
+ list: show the active polls
settings:
settings: poll settings
roles_weights: manage weight for certain roles
@@ -12,7 +13,7 @@ commands:
max_duration: set the maximum duration a poll can be opened
votes: set the default amount of votes a user can have on polls
anonymous: set if user can see who voted on a poll
- everyone: manage role weight for the default role
+ fair: manage if role weights impact on default polls
yes_no: add thumbs-up/down emotes on a message
team_yes_no: starts a yes/no poll and shows, which teamler has not voted yet.
@@ -57,6 +58,11 @@ poll_config:
ev_row: "{} -> `{}x`"
row: "\n<@&{}> -> `{}x`"
+polls:
+ title: Active polls
+ row: "\n[`{}`]({}) by <@{}> until `{} UTC`"
+ team_row: "\n:star: [`{}`]({}) by <@{}> until `{} UTC`"
+
role_weight:
set: "Set vote weight for <@&{}> to `{}`"
reset: "Vote weight has been reset for <@&{}>"
@@ -90,6 +96,10 @@ anonymous:
is_on: made default poll votes anonymous
is_off: made default poll votes visible
+fair:
+ is_on: made default poll votes fair
+ is_off: made default poll votes based on roles
+
select:
place: Select Options
placeholder:
@@ -132,12 +142,18 @@ wizard:
```
--type {standard,team}, -T {standard,team}
standard or team embed [Default: 'standard']
+
--deadline DEADLINE, -D DEADLINE
time when the poll should be closed [Default: server settings]
- --anonymous ANONYMOUS, -A ANONYMOUS
+
+ --anonymous {True,False}, -A {True,False}
people can see who voted or not [Default: server settings]
+
--choices CHOICES, -C CHOICES
the amount of votes someone can set [Default: multiple choices]
+
+ --fair {True,False}, -F {True,False}
+ all roles have the same vote weight [Default: server settings]
```
example:
name: Example
From 593f563aa1ad3a318aede8531e8cac6f26552f4b Mon Sep 17 00:00:00 2001
From: NekoFanatic <83883849+NekoFanatic@users.noreply.github.com>
Date: Mon, 9 May 2022 16:57:33 +0200
Subject: [PATCH 23/44] added persistent views
---
general/polls/cog.py | 114 ++++++++++++++++++++++++++--------------
general/polls/models.py | 5 +-
2 files changed, 80 insertions(+), 39 deletions(-)
diff --git a/general/polls/cog.py b/general/polls/cog.py
index 765c9ecf3..fc723b713 100644
--- a/general/polls/cog.py
+++ b/general/polls/cog.py
@@ -230,6 +230,58 @@ async def get_teamler(guild: Guild, team_roles: list[str]) -> set[Member]:
return teamlers
+async def handle_deleted_messages(bot, message_id: int):
+ deleted_embed: Poll | None = await db.get(Poll, message_id=message_id)
+ deleted_interaction: Poll | None = await db.get(Poll, interaction_message_id=message_id)
+
+ if not deleted_embed and not deleted_interaction:
+ return
+
+ poll = deleted_embed or deleted_interaction
+ channel = await bot.fetch_channel(poll.channel_id)
+ try:
+ if deleted_interaction:
+ msg: Message | None = await channel.fetch_message(poll.message_id)
+ else:
+ msg: Message | None = await channel.fetch_message(poll.interaction_message_id)
+ except NotFound:
+ msg = None
+
+ if msg:
+ await poll.remove()
+ await msg.delete()
+
+
+async def check_poll_time(poll: Poll) -> bool:
+ if not poll.end_time:
+ await poll.remove()
+ return False
+
+ elif poll.end_time < utcnow():
+ return False
+
+ return True
+
+
+async def close_poll(bot, poll: Poll):
+ try:
+ channel = await bot.fetch_channel(poll.channel_id)
+ embed_message = await channel.fetch_message(poll.message_id)
+ interaction_message = await channel.fetch_message(poll.interaction_message_id)
+ except NotFound:
+ poll.active = False
+ return
+
+ await interaction_message.delete()
+ embed = embed_message.embeds[0]
+ embed.set_footer(text=t.footer_closed)
+
+ await embed_message.edit(embed=embed)
+ await embed_message.unpin()
+
+ poll.active = False
+
+
class MySelect(Select):
@db_wrapper
async def callback(self, interaction):
@@ -278,28 +330,6 @@ async def callback(self, interaction):
await message.edit(embed=embed)
-async def handle_deleted_messages(bot, message_id: int):
- deleted_embed: Poll | None = await db.get(Poll, message_id=message_id)
- deleted_interaction: Poll | None = await db.get(Poll, interaction_message_id=message_id)
-
- if not deleted_embed and not deleted_interaction:
- return
-
- poll = deleted_embed or deleted_interaction
- channel = await bot.fetch_channel(poll.channel_id)
- try:
- if deleted_interaction:
- msg: Message | None = await channel.fetch_message(poll.message_id)
- else:
- msg: Message | None = await channel.fetch_message(poll.interaction_message_id)
- except NotFound:
- msg = None
-
- if msg:
- await poll.remove()
- await msg.delete()
-
-
class PollsCog(Cog, name="Polls"):
CONTRIBUTORS = [
Contributor.MaxiHuHe04,
@@ -313,6 +343,25 @@ def __init__(self, team_roles: list[str]):
self.team_roles: list[str] = team_roles
async def on_ready(self):
+ polls: list[Poll] = await db.all(filter_by(Poll, (Poll.options, Option.votes), active=True))
+ for poll in polls:
+ if await check_poll_time(poll):
+ select_obj = MySelect(
+ custom_id=str(poll.message_id),
+ placeholder=t.select.placeholder(cnt=poll.max_choices),
+ max_values=poll.max_choices,
+ options=[
+ SelectOption(
+ label=t.select.label(option.field_position + 1),
+ emoji=option.emote,
+ description=option.option,
+ )
+ for option in poll.options
+ ],
+ )
+
+ self.bot.add_view(view=create_select_view(select_obj), message_id=poll.interaction_message_id)
+
try:
self.poll_loop.start()
except RuntimeError:
@@ -330,22 +379,8 @@ async def poll_loop(self):
polls: list[Poll] = await db.all(filter_by(Poll, active=True))
for poll in polls:
- if not poll.end_time:
- await poll.remove()
- continue
- if poll.end_time < utcnow():
- channel = await self.bot.fetch_channel(poll.channel_id)
- embed_message = await channel.fetch_message(poll.message_id)
- interaction_message = await channel.fetch_message(poll.interaction_message_id)
-
- await interaction_message.delete()
- embed = embed_message.embeds[0]
- embed.set_footer(text=t.footer_closed)
-
- await embed_message.edit(embed=embed)
- await embed_message.unpin()
-
- poll.active = False
+ if not await check_poll_time(poll):
+ await close_poll(self.bot, poll)
@commands.group(name="poll", aliases=["vote"])
@guild_only()
@@ -580,6 +615,7 @@ async def quick(self, ctx: Context, *, args: str):
poll_type=await PollsDefaultSettings.type.get(),
interaction=interaction.id,
fair=await PollsDefaultSettings.fair.get(),
+ max_choices=max_choices,
)
await ctx.message.delete()
@@ -654,6 +690,7 @@ def check(m: Message):
poll_type=poll_type.lower(),
interaction=interaction.id,
fair=fair,
+ max_choices=choices,
)
@commands.command(aliases=["yn"])
@@ -713,4 +750,5 @@ async def team_yesno(self, ctx: Context, *, text: str):
poll_type="team",
interaction=interaction.id,
fair=True,
+ max_choices=1,
)
diff --git a/general/polls/models.py b/general/polls/models.py
index 36875c890..d10856fd9 100644
--- a/general/polls/models.py
+++ b/general/polls/models.py
@@ -31,6 +31,7 @@ class Poll(Base):
can_delete: Union[Column, bool] = Column(Boolean)
fair: Union[Column, bool] = Column(Boolean)
active: Union[Column, bool] = Column(Boolean)
+ max_choices: Union[Column, int] = Column(BigInteger)
@staticmethod
async def create(
@@ -47,6 +48,7 @@ async def create(
poll_type: str,
interaction: int,
fair: bool,
+ max_choices: int,
) -> Poll:
row = Poll(
message_id=message_id,
@@ -63,6 +65,7 @@ async def create(
interaction_message_id=interaction,
fair=fair,
active=True,
+ max_choices=max_choices,
)
for position, poll_option in enumerate(options):
row.options.append(
@@ -86,7 +89,7 @@ class Option(Base):
votes: list[PollVote] = relationship("PollVote", back_populates="option", cascade="all, delete")
poll: Poll = relationship("Poll", back_populates="options")
emote: Union[Column, str] = Column(Text(30))
- option: Union[Column, str] = Column(Text(150))
+ option: Union[Column, str] = Column(Text(250))
field_position: Union[Column, int] = Column(BigInteger)
@staticmethod
From 431098c075e09af42b34adbf5bfad9aa4747b5d8 Mon Sep 17 00:00:00 2001
From: NekoFanatic <83883849+NekoFanatic@users.noreply.github.com>
Date: Fri, 13 May 2022 19:11:15 +0200
Subject: [PATCH 24/44] some changes
---
general/polls/cog.py | 58 +++++++++++++++++++------------
general/polls/models.py | 36 ++++++++++++++++++-
general/polls/translations/en.yml | 8 +++--
3 files changed, 77 insertions(+), 25 deletions(-)
diff --git a/general/polls/cog.py b/general/polls/cog.py
index fc723b713..730a508b4 100644
--- a/general/polls/cog.py
+++ b/general/polls/cog.py
@@ -20,10 +20,10 @@
from discord.ext import commands, tasks
from discord.ext.commands import CommandError, Context, UserInputError, guild_only
from discord.ui import Select, View
-from discord.utils import utcnow
+from discord.utils import format_dt, utcnow
from PyDrocsid.cog import Cog
-from PyDrocsid.command import add_reactions, docs
+from PyDrocsid.command import Confirmation, add_reactions, docs
from PyDrocsid.database import db, db_wrapper, filter_by
from PyDrocsid.embeds import EmbedLimits, send_long_embed
from PyDrocsid.emojis import emoji_to_name, name_to_emoji
@@ -304,10 +304,12 @@ async def callback(self, interaction):
await vote.remove()
opt.votes.remove(vote)
+ ev_pover = await PollsDefaultSettings.everyone_power.get()
if poll.fair:
- user_weight: float = await PollsDefaultSettings.everyone_power.get()
+ user_weight: float = ev_pover
else:
- user_weight: float = 1.0 # TODO: Add function to get user vote weight
+ highest_role = await RoleWeight.get_highest(user.roles)
+ user_weight: float = ev_pover if highest_role < ev_pover else highest_role
for option in new_options:
option.votes.append(
await PollVote.create(option_id=option.id, user_id=user.id, poll_id=poll.id, vote_weight=user_weight)
@@ -336,7 +338,7 @@ class PollsCog(Cog, name="Polls"):
Contributor.Defelo,
Contributor.TNT2k,
Contributor.wolflu,
- Contributor.NekoFanatic, # rewrote most of this code
+ Contributor.NekoFanatic, # rewrote most of this code (Please blame @Defelo for the code)
]
def __init__(self, team_roles: list[str]):
@@ -394,23 +396,25 @@ async def poll(self, ctx: Context):
@docs(t.commands.poll.list)
async def list(self, ctx: Context):
polls: list[Poll] = await db.all(filter_by(Poll, active=True, guild_id=ctx.guild.id))
+ description = ""
if polls:
- description = ""
for poll in polls:
if poll.poll_type == "team" and not await PollsPermission.team_poll.check_permissions(ctx.author):
continue
if poll.poll_type == "team":
description += t.polls.team_row(
- poll.title, poll.message_url, poll.owner_id, poll.end_time.strftime("%Y-%m-%d %H:%M")
+ poll.title, poll.message_url, poll.owner_id, format_dt(poll.end_time, style="R")
)
else:
description += t.polls.row(
- poll.title, poll.message_url, poll.owner_id, poll.end_time.strftime("%Y-%m-%d %H:%M")
+ poll.title, poll.message_url, poll.owner_id, format_dt(poll.end_time, style="R")
)
+ if description:
+ embed: Embed = Embed(title=t.polls.title, description=description, color=Colors.Polls)
+ await send_long_embed(ctx, embed=embed, paginate=True)
- embed: Embed = Embed(title=t.polls.title, description=description, color=Colors.Polls)
-
- await send_long_embed(ctx, embed=embed, paginate=True)
+ if not polls or not description:
+ await send_long_embed(ctx, embed=Embed(title=t.no_polls, color=Colors.error))
@poll.command(name="delete", aliases=["del"])
@docs(t.commands.poll.delete)
@@ -418,14 +422,25 @@ async def delete(self, ctx: Context, message: Message):
poll: Poll = await db.get(Poll, message_id=message.id)
if not poll:
raise CommandError(t.error.not_poll)
- if not await PollsPermission.delete.check_permissions(ctx.author) and not poll.owner_id == ctx.author.id:
+ if (
+ poll.can_delete
+ and not await PollsPermission.delete.check_permissions(ctx.author)
+ and not poll.owner_id == ctx.author.id
+ ):
raise PermissionError
+ elif not poll.can_delete and not poll.owner_id == ctx.author.id:
+ raise PermissionError # if delete is False, only the owner can delete it
+
+ if not await Confirmation().run(ctx, t.delete.confirm_text):
+ return
await message.delete()
await poll.remove()
- interaction_message: Message = await ctx.channel.fetch_message(poll.interaction_message_id)
- if interaction_message:
+ try:
+ interaction_message: Message = await ctx.channel.fetch_message(poll.interaction_message_id)
await interaction_message.delete()
+ except NotFound:
+ pass
await add_reactions(ctx.message, "white_check_mark")
@@ -472,7 +487,9 @@ async def settings(self, ctx: Context):
max_time: int = await PollsDefaultSettings.max_duration.get()
embed.add_field(
name=t.poll_config.duration.name,
- value=t.poll_config.duration.time(cnt=time) if not time <= 0 else t.poll_config.duration.time(cnt=max_time),
+ value=t.poll_config.duration.time(cnt=time)
+ if not time <= 0
+ else t.poll_config.duration.time(cnt=max_time * 24),
inline=False,
)
embed.add_field(
@@ -594,7 +611,7 @@ async def fair(self, ctx: Context, status: bool):
async def quick(self, ctx: Context, *, args: str):
deadline = await PollsDefaultSettings.duration.get()
if deadline == 0:
- deadline = await PollsDefaultSettings.max_duration.get()
+ deadline = await PollsDefaultSettings.max_duration.get() * 24
max_choices = await PollsDefaultSettings.max_choices.get()
anonymous = await PollsDefaultSettings.anonymous.get()
message, interaction, parsed_options, question = await send_poll(
@@ -645,16 +662,13 @@ def check(m: Message):
poll_type = "standard"
if poll_type == "standard":
title: str = t.poll
+ max_deadline = await PollsDefaultSettings.max_duration.get() * 24
deadline: Union[list[str, str], int] = parsed.deadline
if isinstance(deadline, int):
- deadline = (
- deadline
- if deadline >= await PollsDefaultSettings.max_duration.get()
- else await PollsDefaultSettings.max_duration.get()
- )
+ deadline = deadline if deadline <= max_deadline else await max_deadline
else:
if await PollsDefaultSettings.duration.get() == 0:
- deadline = await PollsDefaultSettings.max_duration.get()
+ deadline = max_deadline
else:
deadline = await PollsDefaultSettings.duration.get()
anonymous: bool = parsed.anonymous
diff --git a/general/polls/models.py b/general/polls/models.py
index d10856fd9..79c4d68d4 100644
--- a/general/polls/models.py
+++ b/general/polls/models.py
@@ -3,11 +3,33 @@
from datetime import datetime
from typing import Optional, Union
+from discord import Role
from discord.utils import utcnow
from sqlalchemy import BigInteger, Boolean, Column, Float, ForeignKey, Text
from sqlalchemy.orm import relationship
-from PyDrocsid.database import Base, UTCDateTime, db, filter_by
+from PyDrocsid.database import Base, UTCDateTime, db, filter_by, select
+from PyDrocsid.environment import CACHE_TTL
+from PyDrocsid.redis import redis
+
+
+async def sync_redis(role_id: int = None) -> list[dict[str, int | float]]:
+ out = []
+
+ async with redis.pipeline() as pipe:
+ if role_id:
+ await pipe.delete(key := f"poll_role_weights={role_id}")
+ weights: RoleWeight
+ async for weights in await db.stream(select(RoleWeight)):
+ await pipe.delete(key := f"poll_role_weights={role_id or weights.role_id}")
+ save = {"role": int(weights.role_id), "weight": float(weights.weight)}
+ out.append(save)
+ await pipe.set(key, str(weights.weight))
+ await pipe.expire(key, CACHE_TTL)
+
+ await pipe.execute()
+
+ return out
class Poll(Base):
@@ -132,11 +154,23 @@ class RoleWeight(Base):
async def create(guild_id: int, role: int, weight: float) -> RoleWeight:
role_weight = RoleWeight(guild_id=guild_id, role_id=role, weight=weight, timestamp=utcnow())
await db.add(role_weight)
+ await sync_redis()
return role_weight
async def remove(self) -> None:
await db.delete(self)
+ await sync_redis(self.role_id)
@staticmethod
async def get(guild: int) -> list[RoleWeight]:
return await db.all(filter_by(RoleWeight, guild_id=guild))
+
+ @staticmethod
+ async def get_highest(user_roles: list[Role]) -> float:
+ weights: list[str] = []
+ for role in user_roles:
+ weight = await redis.get(f"poll_role_weights={role.id}")
+ if weight:
+ weights.append(weight)
+ if weights:
+ return float(sorted(weights, key=float, reverse=True)[0])
diff --git a/general/polls/translations/en.yml b/general/polls/translations/en.yml
index 6655eae22..d875814f4 100644
--- a/general/polls/translations/en.yml
+++ b/general/polls/translations/en.yml
@@ -60,8 +60,8 @@ poll_config:
polls:
title: Active polls
- row: "\n[`{}`]({}) by <@{}> until `{} UTC`"
- team_row: "\n:star: [`{}`]({}) by <@{}> until `{} UTC`"
+ row: "\n[`{}`]({}) by <@{}> until {}"
+ team_row: "\n:star: [`{}`]({}) by <@{}> until {}"
role_weight:
set: "Set vote weight for <@&{}> to `{}`"
@@ -107,6 +107,9 @@ select:
many: "Select up to {cnt} options!"
label: "Option {}."
+delete:
+ confirm_text: Are you sure that you want to delete this poll?
+
usage:
poll: |
@@ -182,3 +185,4 @@ footer_closed: Closed
can_not_use_wastebucket_as_option: "You can not use :wastebasket: as option"
foreign_message: "You are not allowed to add yes/no reactions to foreign messages!"
could_not_add_reactions: Could not add reactions because I don't have `add_reactions` permission in {}.
+no_polls: No current active polls
From 562c07b2698f01b233ee1c756b031155bfdf54fd Mon Sep 17 00:00:00 2001
From: NekoFanatic <83883849+NekoFanatic@users.noreply.github.com>
Date: Fri, 13 May 2022 21:59:21 +0200
Subject: [PATCH 25/44] removed unused variable
---
general/polls/models.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/general/polls/models.py b/general/polls/models.py
index 79c4d68d4..828a1732f 100644
--- a/general/polls/models.py
+++ b/general/polls/models.py
@@ -18,7 +18,7 @@ async def sync_redis(role_id: int = None) -> list[dict[str, int | float]]:
async with redis.pipeline() as pipe:
if role_id:
- await pipe.delete(key := f"poll_role_weights={role_id}")
+ await pipe.delete(f"poll_role_weights={role_id}")
weights: RoleWeight
async for weights in await db.stream(select(RoleWeight)):
await pipe.delete(key := f"poll_role_weights={role_id or weights.role_id}")
From b8ef866632d0932b6f52c8f391ffe3aec9890991 Mon Sep 17 00:00:00 2001
From: NekoFanatic <83883849+NekoFanatic@users.noreply.github.com>
Date: Sun, 15 May 2022 16:46:31 +0200
Subject: [PATCH 26/44] some refactoring (thanks @ari)
---
general/polls/cog.py | 86 +++++++++++++------------------
general/polls/translations/en.yml | 2 +-
2 files changed, 36 insertions(+), 52 deletions(-)
diff --git a/general/polls/cog.py b/general/polls/cog.py
index 730a508b4..be3a006c3 100644
--- a/general/polls/cog.py
+++ b/general/polls/cog.py
@@ -32,7 +32,7 @@
from PyDrocsid.util import is_teamler
from .colors import Colors
-from .models import Option, Poll, PollVote, RoleWeight
+from .models import Option, Poll, PollVote, RoleWeight, sync_redis
from .permissions import PollsPermission
from .settings import PollsDefaultSettings
from ...contributor import Contributor
@@ -44,7 +44,7 @@
MAX_OPTIONS = 25 # Discord select menu limit
-default_emojis = [name_to_emoji[f"regional_indicator_{x}"] for x in string.ascii_lowercase]
+DEFAULT_EMOJIS = [name_to_emoji[f"regional_indicator_{x}"] for x in string.ascii_lowercase]
class PollOption:
@@ -52,7 +52,7 @@ def __init__(self, ctx: Context, line: str, number: int):
if not line:
raise CommandError(t.empty_option)
- emoji_candidate, *text = line.lstrip().split(" ")
+ emoji_candidate, *text = line.lstrip().split()
text = " ".join(text)
custom_emoji_match = re.fullmatch(r"", emoji_candidate)
@@ -68,7 +68,7 @@ def __init__(self, ctx: Context, line: str, number: int):
self.emoji = unicode_emoji
self.option = text.strip()
else:
- self.emoji = default_emojis[number]
+ self.emoji = DEFAULT_EMOJIS[number]
self.option = line
def __str__(self):
@@ -83,11 +83,7 @@ def create_select_view(select_obj: Select, timeout: float = None) -> View:
def get_percentage(poll: Poll) -> list[tuple[float, float]]:
- values: list[float] = []
- options = poll.options
-
- for option in options:
- values.append(sum([vote.vote_weight for vote in option.votes]))
+ values: list[float] = [sum([vote.vote_weight for vote in option.votes]) for option in poll.options]
return [(float(value), float(round(((value / sum(values)) * 100), 2))) for value in values]
@@ -132,7 +128,7 @@ async def send_poll(
deadline: Optional[float] = None,
) -> tuple[Message, Message, list[tuple[str, str]], str]:
- if not max_choices or max_choices == 0:
+ if not max_choices:
max_choices = t.poll_config.choices.unlimited
question, *options = [line.replace("\x00", "\n") for line in poll_args.replace("\\\n", "\x00").split("\n") if line]
@@ -169,9 +165,9 @@ async def send_poll(
place = t.select.place
max_value = len(options)
else:
- use = len(options) if max_choices >= len(options) else max_choices
- place: str = t.select.placeholder(cnt=use)
- max_value = use
+ options_amount = len(options) if max_choices >= len(options) else max_choices
+ place: str = t.select.placeholder(cnt=options_amount)
+ max_value = options_amount
msg = await ctx.send(embed=embed)
select_obj = MySelect(
@@ -308,7 +304,7 @@ async def callback(self, interaction):
if poll.fair:
user_weight: float = ev_pover
else:
- highest_role = await RoleWeight.get_highest(user.roles)
+ highest_role = await RoleWeight.get_highest(user.roles) or 0
user_weight: float = ev_pover if highest_role < ev_pover else highest_role
for option in new_options:
option.votes.append(
@@ -345,6 +341,7 @@ def __init__(self, team_roles: list[str]):
self.team_roles: list[str] = team_roles
async def on_ready(self):
+ await sync_redis()
polls: list[Poll] = await db.all(filter_by(Poll, (Poll.options, Option.votes), active=True))
for poll in polls:
if await check_poll_time(poll):
@@ -397,18 +394,17 @@ async def poll(self, ctx: Context):
async def list(self, ctx: Context):
polls: list[Poll] = await db.all(filter_by(Poll, active=True, guild_id=ctx.guild.id))
description = ""
- if polls:
- for poll in polls:
- if poll.poll_type == "team" and not await PollsPermission.team_poll.check_permissions(ctx.author):
- continue
- if poll.poll_type == "team":
- description += t.polls.team_row(
- poll.title, poll.message_url, poll.owner_id, format_dt(poll.end_time, style="R")
- )
- else:
- description += t.polls.row(
- poll.title, poll.message_url, poll.owner_id, format_dt(poll.end_time, style="R")
- )
+ for poll in polls:
+ if poll.poll_type == "team" and not await PollsPermission.team_poll.check_permissions(ctx.author):
+ continue
+ if poll.poll_type == "team":
+ description += t.polls.team_row(
+ poll.title, poll.message_url, poll.owner_id, format_dt(poll.end_time, style="R")
+ )
+ else:
+ description += t.polls.row(
+ poll.title, poll.message_url, poll.owner_id, format_dt(poll.end_time, style="R")
+ )
if description:
embed: Embed = Embed(title=t.polls.title, description=description, color=Colors.Polls)
await send_long_embed(ctx, embed=embed, paginate=True)
@@ -458,13 +454,10 @@ async def voted(self, ctx: Context, message: Message):
):
raise PermissionError
- users: dict[str, list[int]] = {}
+ users = {}
for option in poll.options:
for vote in option.votes:
- try:
- users[str(vote.user_id)].append(option.field_position + 1)
- except KeyError:
- users[str(vote.user_id)] = [option.field_position + 1]
+ users[str(vote.user_id)] = users.get(str(vote.user_id), default=[]).append(option.field_position + 1)
description = ""
for key, value in users.items():
@@ -507,7 +500,7 @@ async def settings(self, ctx: Context):
everyone: int = await PollsDefaultSettings.everyone_power.get()
base: str = t.poll_config.roles.ev_row(ctx.guild.default_role, everyone)
if roles:
- base += "".join([t.poll_config.roles.row(role.role_id, role.weight) for role in roles])
+ base += "".join(t.poll_config.roles.row(role.role_id, role.weight) for role in roles)
embed.add_field(name=t.poll_config.roles.name, value=base, inline=False)
await send_long_embed(ctx, embed, paginate=False)
@@ -555,8 +548,7 @@ async def duration(self, ctx: Context, hours: int = None):
@PollsPermission.write.check
@docs(t.commands.poll.settings.max_duration)
async def max_duration(self, ctx: Context, days: int = None):
- if not days:
- days = 7
+ days = days or 7
msg: str = t.max_duration.set(cnt=days)
await PollsDefaultSettings.max_duration.set(days)
@@ -609,9 +601,7 @@ async def fair(self, ctx: Context, status: bool):
@poll.command(name="quick", usage=t.usage.poll, aliases=["q"])
@docs(t.commands.poll.quick)
async def quick(self, ctx: Context, *, args: str):
- deadline = await PollsDefaultSettings.duration.get()
- if deadline == 0:
- deadline = await PollsDefaultSettings.max_duration.get() * 24
+ deadline = await PollsDefaultSettings.duration.get() or await PollsDefaultSettings.max_duration.get() * 24
max_choices = await PollsDefaultSettings.max_choices.get()
anonymous = await PollsDefaultSettings.anonymous.get()
message, interaction, parsed_options, question = await send_poll(
@@ -640,11 +630,8 @@ async def quick(self, ctx: Context, *, args: str):
@poll.command(name="new", usage=t.usage.poll)
@docs(t.commands.poll.new)
async def new(self, ctx: Context, *, options: str):
- def check(m: Message):
- return m.author == ctx.author
-
wizard = await ctx.send(embed=build_wizard())
- mess: Message = await self.bot.wait_for("message", check=check, timeout=60.0)
+ mess: Message = await self.bot.wait_for("message", check=lambda m: m.author == ctx.author, timeout=60.0)
args = mess.content
if args.lower() == t.skip.message:
@@ -654,23 +641,20 @@ def check(m: Message):
await mess.delete()
parser = await get_parser()
- parsed: Namespace = parser.parse_known_args(args.split(" "))[0]
+ parsed: Namespace = parser.parse_known_args(args.split())[0]
- title: str = t.team_poll
+ title: str = t.poll
poll_type: str = parsed.type
- if poll_type.lower() == "team" and not await PollsPermission.team_poll.check_permissions(ctx.author):
+ if poll_type.lower() == "team" and await PollsPermission.team_poll.check_permissions(ctx.author):
+ title: str = t.team_poll
+ else:
poll_type = "standard"
- if poll_type == "standard":
- title: str = t.poll
max_deadline = await PollsDefaultSettings.max_duration.get() * 24
deadline: Union[list[str, str], int] = parsed.deadline
if isinstance(deadline, int):
- deadline = deadline if deadline <= max_deadline else await max_deadline
+ deadline = deadline or max_deadline if deadline <= max_deadline else await max_deadline
else:
- if await PollsDefaultSettings.duration.get() == 0:
- deadline = max_deadline
- else:
- deadline = await PollsDefaultSettings.duration.get()
+ deadline = await PollsDefaultSettings.duration.get() or await PollsDefaultSettings.max_duration.get() * 24
anonymous: bool = parsed.anonymous
choices: int = parsed.choices
diff --git a/general/polls/translations/en.yml b/general/polls/translations/en.yml
index d875814f4..ec2821503 100644
--- a/general/polls/translations/en.yml
+++ b/general/polls/translations/en.yml
@@ -180,7 +180,7 @@ teampoll_all_voted: "All teamlers voted :white_check_mark:"
teamlers_missing:
one: "{last} hasn't voted yet."
many: "{teamlers} and {last} haven't voted yet."
-footer: Ends at `{}`
+footer: Ends at {} UTC
footer_closed: Closed
can_not_use_wastebucket_as_option: "You can not use :wastebasket: as option"
foreign_message: "You are not allowed to add yes/no reactions to foreign messages!"
From 7eb24f8652b1dac68892e4bc69476c560f394684 Mon Sep 17 00:00:00 2001
From: NekoFanatic <83883849+NekoFanatic@users.noreply.github.com>
Date: Mon, 16 May 2022 17:28:16 +0200
Subject: [PATCH 27/44] resolved Tristans change requests
---
general/polls/cog.py | 13 +++++++++++++
general/polls/translations/en.yml | 4 ++--
2 files changed, 15 insertions(+), 2 deletions(-)
diff --git a/general/polls/cog.py b/general/polls/cog.py
index be3a006c3..ade624a9c 100644
--- a/general/polls/cog.py
+++ b/general/polls/cog.py
@@ -76,6 +76,7 @@ def __str__(self):
def create_select_view(select_obj: Select, timeout: float = None) -> View:
+ """returns a view object"""
view = View(timeout=timeout)
view.add_item(select_obj)
@@ -83,12 +84,14 @@ def create_select_view(select_obj: Select, timeout: float = None) -> View:
def get_percentage(poll: Poll) -> list[tuple[float, float]]:
+ """returns the amount of votes and the percentage of an option"""
values: list[float] = [sum([vote.vote_weight for vote in option.votes]) for option in poll.options]
return [(float(value), float(round(((value / sum(values)) * 100), 2))) for value in values]
def build_wizard(skip: bool = False) -> Embed:
+ """creates a help embed for setting up advanced polls"""
if skip:
return Embed(title=t.skip.title, description=t.skip.description, color=Colors.Polls)
@@ -101,6 +104,7 @@ def build_wizard(skip: bool = False) -> Embed:
async def get_parser() -> ArgumentParser:
+ """creates a parser object with options for advanced polls"""
parser = ArgumentParser()
parser.add_argument("--type", "-T", default="standard", choices=["standard", "team"], type=str)
parser.add_argument("--deadline", "-D", default=await PollsDefaultSettings.duration.get(), type=int)
@@ -114,6 +118,7 @@ async def get_parser() -> ArgumentParser:
def calc_end_time(duration: Optional[float]) -> Optional[datetime]:
+ """returns the time when a poll should it from hours"""
if duration != 0 and not None:
return utcnow() + relativedelta(hours=int(duration))
return
@@ -127,6 +132,7 @@ async def send_poll(
field: Optional[tuple[str, str]] = None,
deadline: Optional[float] = None,
) -> tuple[Message, Message, list[tuple[str, str]], str]:
+ """sends a poll embed + view message containing the select field"""
if not max_choices:
max_choices = t.poll_config.choices.unlimited
@@ -196,6 +202,7 @@ async def send_poll(
async def edit_poll_embed(embed: Embed, poll: Poll, missing: list[Member] = None) -> Embed:
+ """edits the poll embed, updating the votes and percentages"""
calc = get_percentage(poll)
for index, field in enumerate(embed.fields):
if field.name == tg.status:
@@ -216,6 +223,7 @@ async def edit_poll_embed(embed: Embed, poll: Poll, missing: list[Member] = None
async def get_teamler(guild: Guild, team_roles: list[str]) -> set[Member]:
+ """gets a list of all team members"""
teamlers: set[Member] = set()
for role_name in team_roles:
if not (team_role := guild.get_role(await RoleSettings.get(role_name))):
@@ -227,6 +235,7 @@ async def get_teamler(guild: Guild, team_roles: list[str]) -> set[Member]:
async def handle_deleted_messages(bot, message_id: int):
+ """if a message containing a poll gets deleted, this function deletes the interaction message (both direction)"""
deleted_embed: Poll | None = await db.get(Poll, message_id=message_id)
deleted_interaction: Poll | None = await db.get(Poll, interaction_message_id=message_id)
@@ -249,6 +258,7 @@ async def handle_deleted_messages(bot, message_id: int):
async def check_poll_time(poll: Poll) -> bool:
+ """checks if a poll has ended"""
if not poll.end_time:
await poll.remove()
return False
@@ -260,6 +270,7 @@ async def check_poll_time(poll: Poll) -> bool:
async def close_poll(bot, poll: Poll):
+ """deletes the interaction message and edits the footer of the poll embed"""
try:
channel = await bot.fetch_channel(poll.channel_id)
embed_message = await channel.fetch_message(poll.message_id)
@@ -279,6 +290,8 @@ async def close_poll(bot, poll: Poll):
class MySelect(Select):
+ """adds a method for handling interactions with the select menu"""
+
@db_wrapper
async def callback(self, interaction):
user = interaction.user
diff --git a/general/polls/translations/en.yml b/general/polls/translations/en.yml
index ec2821503..bbb003b85 100644
--- a/general/polls/translations/en.yml
+++ b/general/polls/translations/en.yml
@@ -167,7 +167,7 @@ wizard:
poll: Poll
team_poll: Team Poll
-team_yn_poll_forbidden: You are not allowed to use a team poll
+team_yn_poll_forbidden: You are not allowed to use a team poll!
vote_explanation: Vote using the reactions below!
too_many_options: You specified too many options. The maximum amount is {}.
option_too_long: Options are limited to {} characters.
@@ -185,4 +185,4 @@ footer_closed: Closed
can_not_use_wastebucket_as_option: "You can not use :wastebasket: as option"
foreign_message: "You are not allowed to add yes/no reactions to foreign messages!"
could_not_add_reactions: Could not add reactions because I don't have `add_reactions` permission in {}.
-no_polls: No current active polls
+no_polls: No current active polls.
From 1e207f7ee8909eec2c6afb7976968231c09e9be1 Mon Sep 17 00:00:00 2001
From: NekoFanatic <83883849+NekoFanatic@users.noreply.github.com>
Date: Mon, 16 May 2022 21:16:30 +0200
Subject: [PATCH 28/44] Resolved some conversations
---
general/polls/cog.py | 34 +++++++++++++++-------------------
general/polls/models.py | 17 +++++++++--------
2 files changed, 24 insertions(+), 27 deletions(-)
diff --git a/general/polls/cog.py b/general/polls/cog.py
index ade624a9c..2d81de8a5 100644
--- a/general/polls/cog.py
+++ b/general/polls/cog.py
@@ -186,9 +186,9 @@ async def send_poll(
],
)
view_msg = await ctx.send(view=create_select_view(select_obj=select_obj))
- parsed_options: list[tuple[str, str]] = [
- (obj.emoji, t.select.label(index + 1)) for index, obj in enumerate(options)
- ]
+
+ parsed_options: list[tuple[str, str]] = [(obj.emoji, t.select.label(ix)) for ix, obj in enumerate(options, start=1)]
+
try:
await msg.pin()
except HTTPException:
@@ -222,7 +222,7 @@ async def edit_poll_embed(embed: Embed, poll: Poll, missing: list[Member] = None
return embed
-async def get_teamler(guild: Guild, team_roles: list[str]) -> set[Member]:
+async def get_staff(guild: Guild, team_roles: list[str]) -> set[Member]:
"""gets a list of all team members"""
teamlers: set[Member] = set()
for role_name in team_roles:
@@ -299,11 +299,11 @@ async def callback(self, interaction):
message: Message = await interaction.channel.fetch_message(interaction.custom_id)
embed: Embed = message.embeds[0] if message.embeds else None
poll: Poll = await db.get(Poll, (Poll.options, Option.votes), message_id=message.id)
+
if not poll or not embed:
return
- options: list[Option] = poll.options
- new_options: list[Option] = [option for option in options if option.option in selected_options]
+ new_options: list[Option] = [option for option in poll.options if option.option in selected_options]
missing: list[Member] | None = None
opt: Option
@@ -319,12 +319,14 @@ async def callback(self, interaction):
else:
highest_role = await RoleWeight.get_highest(user.roles) or 0
user_weight: float = ev_pover if highest_role < ev_pover else highest_role
+
for option in new_options:
option.votes.append(
await PollVote.create(option_id=option.id, user_id=user.id, poll_id=poll.id, vote_weight=user_weight)
)
+
if poll.poll_type == "team":
- teamlers: set[Member] = await get_teamler(interaction.guild, ["team"])
+ teamlers: set[Member] = await get_staff(interaction.guild, ["team"])
if user not in teamlers:
await interaction.response.send_message(content=t.team_yn_poll_forbidden, ephemeral=True)
return
@@ -404,7 +406,7 @@ async def poll(self, ctx: Context):
@poll.command(name="list", aliases=["l"])
@guild_only()
@docs(t.commands.poll.list)
- async def list(self, ctx: Context):
+ async def poll_list(self, ctx: Context):
polls: list[Poll] = await db.all(filter_by(Poll, active=True, guild_id=ctx.guild.id))
description = ""
for poll in polls:
@@ -589,10 +591,7 @@ async def votes(self, ctx: Context, votes: int = None):
@PollsPermission.write.check
@docs(t.commands.poll.settings.anonymous)
async def anonymous(self, ctx: Context, status: bool):
- if status:
- msg: str = t.anonymous.is_on
- else:
- msg: str = t.anonymous.is_off
+ msg: str = t.anonymous.is_on if status else t.anonymous.is_off
await PollsDefaultSettings.anonymous.set(status)
await add_reactions(ctx.message, "white_check_mark")
@@ -602,10 +601,7 @@ async def anonymous(self, ctx: Context, status: bool):
@PollsPermission.write.check
@docs(t.commands.poll.settings.fair)
async def fair(self, ctx: Context, status: bool):
- if status:
- msg: str = t.fair.is_on
- else:
- msg: str = t.fair.is_off
+ msg: str = t.fair.is_on if status else t.fair.is_off
await PollsDefaultSettings.fair.set(status)
await add_reactions(ctx.message, "white_check_mark")
@@ -665,7 +661,7 @@ async def new(self, ctx: Context, *, options: str):
max_deadline = await PollsDefaultSettings.max_duration.get() * 24
deadline: Union[list[str, str], int] = parsed.deadline
if isinstance(deadline, int):
- deadline = deadline or max_deadline if deadline <= max_deadline else await max_deadline
+ deadline = deadline or max_deadline if deadline <= max_deadline else max_deadline
else:
deadline = await PollsDefaultSettings.duration.get() or await PollsDefaultSettings.max_duration.get() * 24
anonymous: bool = parsed.anonymous
@@ -673,7 +669,7 @@ async def new(self, ctx: Context, *, options: str):
if poll_type.lower() == "team":
can_delete, fair = False, True
- missing = list(await get_teamler(self.bot.guilds[0], ["team"]))
+ missing = list(await get_staff(self.bot.guilds[0], ["team"]))
missing.sort(key=lambda m: str(m).lower())
*teamlers, last = (x.mention for x in missing)
teamlers: list[str]
@@ -733,7 +729,7 @@ async def yesno(self, ctx: Context, message: Optional[Message] = None, text: Opt
async def team_yesno(self, ctx: Context, *, text: str):
options = t.yes_no.option_string(text)
- missing = list(await get_teamler(self.bot.guilds[0], ["team"]))
+ missing = list(await get_staff(self.bot.guilds[0], ["team"]))
missing.sort(key=lambda m: str(m).lower())
*teamlers, last = (x.mention for x in missing)
teamlers: list[str]
diff --git a/general/polls/models.py b/general/polls/models.py
index 828a1732f..7da45722a 100644
--- a/general/polls/models.py
+++ b/general/polls/models.py
@@ -18,10 +18,10 @@ async def sync_redis(role_id: int = None) -> list[dict[str, int | float]]:
async with redis.pipeline() as pipe:
if role_id:
- await pipe.delete(f"poll_role_weights={role_id}")
+ await pipe.delete(f"poll_role_weight={role_id}")
weights: RoleWeight
async for weights in await db.stream(select(RoleWeight)):
- await pipe.delete(key := f"poll_role_weights={role_id or weights.role_id}")
+ await pipe.delete(key := f"poll_role_weight={role_id or weights.role_id}")
save = {"role": int(weights.role_id), "weight": float(weights.weight)}
out.append(save)
await pipe.set(key, str(weights.weight))
@@ -167,10 +167,11 @@ async def get(guild: int) -> list[RoleWeight]:
@staticmethod
async def get_highest(user_roles: list[Role]) -> float:
- weights: list[str] = []
+ weight: float = 0.0
for role in user_roles:
- weight = await redis.get(f"poll_role_weights={role.id}")
- if weight:
- weights.append(weight)
- if weights:
- return float(sorted(weights, key=float, reverse=True)[0])
+ _weight = await redis.get(f"poll_role_weight={role.id}")
+
+ if _weight and weight < (_weight := float(_weight)):
+ weight = _weight
+
+ return weight
From fb3906386516ccd5b7da514f65354073f8d311e6 Mon Sep 17 00:00:00 2001
From: NekoFanatic <83883849+NekoFanatic@users.noreply.github.com>
Date: Mon, 16 May 2022 22:10:29 +0200
Subject: [PATCH 29/44] Rewrote code to use Enums
---
general/polls/cog.py | 32 ++++++++++++++++++++------------
general/polls/models.py | 12 +++++++++---
general/polls/settings.py | 1 -
3 files changed, 29 insertions(+), 16 deletions(-)
diff --git a/general/polls/cog.py b/general/polls/cog.py
index 2d81de8a5..6d9031880 100644
--- a/general/polls/cog.py
+++ b/general/polls/cog.py
@@ -2,6 +2,7 @@
import string
from argparse import ArgumentParser, Namespace
from datetime import datetime
+from enum import Enum
from typing import Optional, Union
from dateutil.relativedelta import relativedelta
@@ -32,7 +33,7 @@
from PyDrocsid.util import is_teamler
from .colors import Colors
-from .models import Option, Poll, PollVote, RoleWeight, sync_redis
+from .models import Option, Poll, PollType, PollVote, RoleWeight, sync_redis
from .permissions import PollsPermission
from .settings import PollsDefaultSettings
from ...contributor import Contributor
@@ -106,7 +107,13 @@ def build_wizard(skip: bool = False) -> Embed:
async def get_parser() -> ArgumentParser:
"""creates a parser object with options for advanced polls"""
parser = ArgumentParser()
- parser.add_argument("--type", "-T", default="standard", choices=["standard", "team"], type=str)
+ parser.add_argument(
+ "--type",
+ "-T",
+ default=PollType.STANDARD.value,
+ choices=[PollType.STANDARD.value, PollType.TEAM.value],
+ type=str,
+ )
parser.add_argument("--deadline", "-D", default=await PollsDefaultSettings.duration.get(), type=int)
parser.add_argument(
"--anonymous", "-A", default=await PollsDefaultSettings.anonymous.get(), type=bool, choices=[True, False]
@@ -325,7 +332,7 @@ async def callback(self, interaction):
await PollVote.create(option_id=option.id, user_id=user.id, poll_id=poll.id, vote_weight=user_weight)
)
- if poll.poll_type == "team":
+ if poll.poll_type == PollType.TEAM:
teamlers: set[Member] = await get_staff(interaction.guild, ["team"])
if user not in teamlers:
await interaction.response.send_message(content=t.team_yn_poll_forbidden, ephemeral=True)
@@ -410,9 +417,9 @@ async def poll_list(self, ctx: Context):
polls: list[Poll] = await db.all(filter_by(Poll, active=True, guild_id=ctx.guild.id))
description = ""
for poll in polls:
- if poll.poll_type == "team" and not await PollsPermission.team_poll.check_permissions(ctx.author):
+ if poll.poll_type == PollType.TEAM and not await PollsPermission.team_poll.check_permissions(ctx.author):
continue
- if poll.poll_type == "team":
+ if poll.poll_type == PollType.TEAM:
description += t.polls.team_row(
poll.title, poll.message_url, poll.owner_id, format_dt(poll.end_time, style="R")
)
@@ -628,7 +635,7 @@ async def quick(self, ctx: Context, *, args: str):
anonymous=anonymous,
can_delete=True,
options=parsed_options,
- poll_type=await PollsDefaultSettings.type.get(),
+ poll_type=PollType.STANDARD,
interaction=interaction.id,
fair=await PollsDefaultSettings.fair.get(),
max_choices=max_choices,
@@ -653,11 +660,12 @@ async def new(self, ctx: Context, *, options: str):
parsed: Namespace = parser.parse_known_args(args.split())[0]
title: str = t.poll
- poll_type: str = parsed.type
- if poll_type.lower() == "team" and await PollsPermission.team_poll.check_permissions(ctx.author):
+ poll_type: Enum | str = parsed.type.lower()
+ if poll_type == PollType.TEAM.value and await PollsPermission.team_poll.check_permissions(ctx.author):
+ poll_type = PollType.TEAM
title: str = t.team_poll
else:
- poll_type = "standard"
+ poll_type = PollType.STANDARD
max_deadline = await PollsDefaultSettings.max_duration.get() * 24
deadline: Union[list[str, str], int] = parsed.deadline
if isinstance(deadline, int):
@@ -667,7 +675,7 @@ async def new(self, ctx: Context, *, options: str):
anonymous: bool = parsed.anonymous
choices: int = parsed.choices
- if poll_type.lower() == "team":
+ if poll_type == PollType.TEAM:
can_delete, fair = False, True
missing = list(await get_staff(self.bot.guilds[0], ["team"]))
missing.sort(key=lambda m: str(m).lower())
@@ -694,7 +702,7 @@ async def new(self, ctx: Context, *, options: str):
anonymous=anonymous,
can_delete=can_delete,
options=parsed_options,
- poll_type=poll_type.lower(),
+ poll_type=poll_type,
interaction=interaction.id,
fair=fair,
max_choices=choices,
@@ -754,7 +762,7 @@ async def team_yesno(self, ctx: Context, *, text: str):
anonymous=False,
can_delete=False,
options=parsed_options,
- poll_type="team",
+ poll_type=PollType.TEAM,
interaction=interaction.id,
fair=True,
max_choices=1,
diff --git a/general/polls/models.py b/general/polls/models.py
index 7da45722a..af0202cfd 100644
--- a/general/polls/models.py
+++ b/general/polls/models.py
@@ -1,11 +1,12 @@
from __future__ import annotations
+import enum
from datetime import datetime
from typing import Optional, Union
from discord import Role
from discord.utils import utcnow
-from sqlalchemy import BigInteger, Boolean, Column, Float, ForeignKey, Text
+from sqlalchemy import BigInteger, Boolean, Column, Enum, Float, ForeignKey, Text
from sqlalchemy.orm import relationship
from PyDrocsid.database import Base, UTCDateTime, db, filter_by, select
@@ -13,6 +14,11 @@
from PyDrocsid.redis import redis
+class PollType(enum.Enum):
+ TEAM = "team"
+ STANDARD = "standard"
+
+
async def sync_redis(role_id: int = None) -> list[dict[str, int | float]]:
out = []
@@ -47,7 +53,7 @@ class Poll(Base):
owner_id: Union[Column, int] = Column(BigInteger)
timestamp: Union[Column, datetime] = Column(UTCDateTime)
title: Union[Column, str] = Column(Text(256))
- poll_type: Union[Column, str] = Column(Text(50))
+ poll_type: Union[Column, PollType] = Column(Enum(PollType))
end_time: Union[Column, datetime] = Column(UTCDateTime)
anonymous: Union[Column, bool] = Column(Boolean)
can_delete: Union[Column, bool] = Column(Boolean)
@@ -67,7 +73,7 @@ async def create(
end: Optional[datetime],
anonymous: bool,
can_delete: bool,
- poll_type: str,
+ poll_type: enum.Enum,
interaction: int,
fair: bool,
max_choices: int,
diff --git a/general/polls/settings.py b/general/polls/settings.py
index 90e552cd1..c5d7a646f 100644
--- a/general/polls/settings.py
+++ b/general/polls/settings.py
@@ -5,7 +5,6 @@ class PollsDefaultSettings(Settings):
duration = 0 # 0 for max_duration duration (duration in hours)
max_duration = 7 # max duration (duration in days)
max_choices = 0 # 0 for unlimited choices
- type = "standard"
everyone_power = 1.0
anonymous = False
fair = False
From b820dbbc41c476d82d86baad88ea86a6c659c774 Mon Sep 17 00:00:00 2001
From: NekoFanatic <83883849+NekoFanatic@users.noreply.github.com>
Date: Mon, 16 May 2022 22:49:43 +0200
Subject: [PATCH 30/44] sOmE chAngEs
---
general/polls/cog.py | 2 ++
general/polls/translations/en.yml | 4 +++-
2 files changed, 5 insertions(+), 1 deletion(-)
diff --git a/general/polls/cog.py b/general/polls/cog.py
index 6d9031880..1f7ec8c70 100644
--- a/general/polls/cog.py
+++ b/general/polls/cog.py
@@ -518,6 +518,8 @@ async def settings(self, ctx: Context):
)
anonymous: bool = await PollsDefaultSettings.anonymous.get()
embed.add_field(name=t.poll_config.anonymous.name, value=str(anonymous), inline=False)
+ fair: bool = await PollsDefaultSettings.fair.get()
+ embed.add_field(name=t.poll_config.fair.name, value=str(fair), inline=False)
roles = await RoleWeight.get(ctx.guild.id)
everyone: int = await PollsDefaultSettings.everyone_power.get()
base: str = t.poll_config.roles.ev_row(ctx.guild.default_role, everyone)
diff --git a/general/polls/translations/en.yml b/general/polls/translations/en.yml
index bbb003b85..b08b30a87 100644
--- a/general/polls/translations/en.yml
+++ b/general/polls/translations/en.yml
@@ -52,7 +52,9 @@ poll_config:
many: "{cnt} choices per user"
unlimited: unlimited
anonymous:
- name: "Anonymous"
+ name: "**Anonymous**"
+ fair:
+ name: "**Fair polls**"
roles:
name: "**Role Weights**"
ev_row: "{} -> `{}x`"
From f32c85fe851a2df76512d5cc5cb8eade8f389e2b Mon Sep 17 00:00:00 2001
From: NekoFanatic <83883849+NekoFanatic@users.noreply.github.com>
Date: Thu, 19 May 2022 16:16:03 +0200
Subject: [PATCH 31/44] SoMe CHaNGeS
---
general/polls/cog.py | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/general/polls/cog.py b/general/polls/cog.py
index 1f7ec8c70..adff54fb0 100644
--- a/general/polls/cog.py
+++ b/general/polls/cog.py
@@ -54,20 +54,18 @@ def __init__(self, ctx: Context, line: str, number: int):
raise CommandError(t.empty_option)
emoji_candidate, *text = line.lstrip().split()
- text = " ".join(text)
+ self.option = " ".join(text)
custom_emoji_match = re.fullmatch(r"", emoji_candidate)
+
if custom_emoji := ctx.bot.get_emoji(int(custom_emoji_match.group(1))) if custom_emoji_match else None:
self.emoji = str(custom_emoji)
- self.option = text.strip()
elif (unicode_emoji := emoji_candidate) in emoji_to_name:
self.emoji = unicode_emoji
- self.option = text.strip()
elif (match := re.match(r"^:([^: ]+):$", emoji_candidate)) and (
unicode_emoji := name_to_emoji.get(match.group(1).replace(":", ""))
):
self.emoji = unicode_emoji
- self.option = text.strip()
else:
self.emoji = DEFAULT_EMOJIS[number]
self.option = line
From 2d84562259f84d9021ebe328d756e9d15313a798 Mon Sep 17 00:00:00 2001
From: NekoFanatic <83883849+NekoFanatic@users.noreply.github.com>
Date: Mon, 23 May 2022 12:06:32 +0200
Subject: [PATCH 32/44] wer das hier liest kann lesen
---
general/polls/cog.py | 37 ++++++++++++++++++-------------------
1 file changed, 18 insertions(+), 19 deletions(-)
diff --git a/general/polls/cog.py b/general/polls/cog.py
index adff54fb0..b9545b432 100644
--- a/general/polls/cog.py
+++ b/general/polls/cog.py
@@ -1,4 +1,3 @@
-import re
import string
from argparse import ArgumentParser, Namespace
from datetime import datetime
@@ -19,7 +18,7 @@
SelectOption,
)
from discord.ext import commands, tasks
-from discord.ext.commands import CommandError, Context, UserInputError, guild_only
+from discord.ext.commands import CommandError, Context, EmojiConverter, EmojiNotFound, UserInputError, guild_only
from discord.ui import Select, View
from discord.utils import format_dt, utcnow
@@ -49,26 +48,26 @@
class PollOption:
- def __init__(self, ctx: Context, line: str, number: int):
+ emoji: str = None
+ option: str = None
+
+ async def init(self, ctx: Context, line: str, number: int):
if not line:
raise CommandError(t.empty_option)
- emoji_candidate, *text = line.lstrip().split()
- self.option = " ".join(text)
-
- custom_emoji_match = re.fullmatch(r"", emoji_candidate)
+ emoji_candidate, *option = line.split()
+ option = " ".join(option)
+ try:
+ self.emoji = str(await EmojiConverter().convert(ctx, emoji_candidate))
+ except EmojiNotFound:
+ if (unicode_emoji := emoji_candidate) in emoji_to_name:
+ self.emoji = unicode_emoji
+ else:
+ self.emoji = DEFAULT_EMOJIS[number]
+ option = f"{emoji_candidate} {option}"
+ self.option = option
- if custom_emoji := ctx.bot.get_emoji(int(custom_emoji_match.group(1))) if custom_emoji_match else None:
- self.emoji = str(custom_emoji)
- elif (unicode_emoji := emoji_candidate) in emoji_to_name:
- self.emoji = unicode_emoji
- elif (match := re.match(r"^:([^: ]+):$", emoji_candidate)) and (
- unicode_emoji := name_to_emoji.get(match.group(1).replace(":", ""))
- ):
- self.emoji = unicode_emoji
- else:
- self.emoji = DEFAULT_EMOJIS[number]
- self.option = line
+ return self
def __str__(self):
return f"{self.emoji} {self.option}" if self.option else self.emoji
@@ -151,7 +150,7 @@ async def send_poll(
if field and len(options) >= MAX_OPTIONS:
raise CommandError(t.too_many_options(MAX_OPTIONS - 1))
- options = [PollOption(ctx, line, i) for i, line in enumerate(options)]
+ options = [await PollOption().init(ctx, line, i) for i, line in enumerate(options)]
if any(len(str(option)) > EmbedLimits.FIELD_VALUE for option in options):
raise CommandError(t.option_too_long(EmbedLimits.FIELD_VALUE))
From 7ab5ce4a86535b04f190166a49783e054aec1521 Mon Sep 17 00:00:00 2001
From: NekoFanatic <83883849+NekoFanatic@users.noreply.github.com>
Date: Mon, 6 Jun 2022 12:59:29 +0200
Subject: [PATCH 33/44] some changes
---
general/polls/cog.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/general/polls/cog.py b/general/polls/cog.py
index b9545b432..8313d42d8 100644
--- a/general/polls/cog.py
+++ b/general/polls/cog.py
@@ -162,7 +162,7 @@ async def send_poll(
end_time = calc_end_time(deadline)
embed.set_footer(text=t.footer(end_time.strftime("%Y-%m-%d %H:%M")))
- if len(set(map(lambda x: x.emoji, options))) < len(options):
+ if len({option.emoji for option in options}) < len(options):
raise CommandError(t.option_duplicated)
for option in options:
@@ -586,7 +586,7 @@ async def votes(self, ctx: Context, votes: int = None):
else:
msg: str = t.votes.set(cnt=votes)
- if not 0 < votes < 25:
+ if not 0 < votes < MAX_OPTIONS:
votes = 0
await PollsDefaultSettings.max_choices.set(votes)
From 3a137844d722a5b5858b4d90862f3eb3f3ed0309 Mon Sep 17 00:00:00 2001
From: NekoFanatic <83883849+NekoFanatic@users.noreply.github.com>
Date: Mon, 6 Jun 2022 20:39:19 +0200
Subject: [PATCH 34/44] some changes
---
general/polls/cog.py | 9 ++++++---
general/polls/translations/en.yml | 1 +
2 files changed, 7 insertions(+), 3 deletions(-)
diff --git a/general/polls/cog.py b/general/polls/cog.py
index 8313d42d8..ce2560462 100644
--- a/general/polls/cog.py
+++ b/general/polls/cog.py
@@ -115,7 +115,9 @@ async def get_parser() -> ArgumentParser:
parser.add_argument(
"--anonymous", "-A", default=await PollsDefaultSettings.anonymous.get(), type=bool, choices=[True, False]
)
- parser.add_argument("--choices", "-C", default=await PollsDefaultSettings.max_choices.get(), type=int)
+ parser.add_argument(
+ "--choices", "-C", default=await PollsDefaultSettings.max_choices.get() or MAX_OPTIONS, type=int
+ )
parser.add_argument("--fair", "-F", default=await PollsDefaultSettings.fair.get(), type=bool, choices=[True, False])
return parser
@@ -345,6 +347,7 @@ async def callback(self, interaction):
embed = await edit_poll_embed(embed, poll, missing)
await message.edit(embed=embed)
+ await interaction.response.send_message(content=t.poll_voted, ephemeral=True)
class PollsCog(Cog, name="Polls"):
@@ -507,7 +510,7 @@ async def settings(self, ctx: Context):
embed.add_field(
name=t.poll_config.max_duration.name, value=t.poll_config.max_duration.time(cnt=max_time), inline=False
)
- choice: int = await PollsDefaultSettings.max_choices.get()
+ choice: int = await PollsDefaultSettings.max_choices.get() or MAX_OPTIONS
embed.add_field(
name=t.poll_config.choices.name,
value=t.poll_config.choices.amount(cnt=choice) if not choice <= 0 else t.poll_config.choices.unlimited,
@@ -617,7 +620,7 @@ async def fair(self, ctx: Context, status: bool):
@docs(t.commands.poll.quick)
async def quick(self, ctx: Context, *, args: str):
deadline = await PollsDefaultSettings.duration.get() or await PollsDefaultSettings.max_duration.get() * 24
- max_choices = await PollsDefaultSettings.max_choices.get()
+ max_choices = await PollsDefaultSettings.max_choices.get() or MAX_OPTIONS
anonymous = await PollsDefaultSettings.anonymous.get()
message, interaction, parsed_options, question = await send_poll(
ctx=ctx, title=t.poll, poll_args=args, max_choices=max_choices, deadline=deadline
diff --git a/general/polls/translations/en.yml b/general/polls/translations/en.yml
index b08b30a87..d228f7512 100644
--- a/general/polls/translations/en.yml
+++ b/general/polls/translations/en.yml
@@ -169,6 +169,7 @@ wizard:
poll: Poll
team_poll: Team Poll
+poll_voted: "Vote was added to the poll"
team_yn_poll_forbidden: You are not allowed to use a team poll!
vote_explanation: Vote using the reactions below!
too_many_options: You specified too many options. The maximum amount is {}.
From 7c7ee07d14c9e7446d9f5dd2aba1d8a739d7516b Mon Sep 17 00:00:00 2001
From: NekoFanatic <83883849+NekoFanatic@users.noreply.github.com>
Date: Mon, 6 Jun 2022 20:42:19 +0200
Subject: [PATCH 35/44] some changes
---
general/polls/cog.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/general/polls/cog.py b/general/polls/cog.py
index ce2560462..c98dcdacc 100644
--- a/general/polls/cog.py
+++ b/general/polls/cog.py
@@ -124,7 +124,7 @@ async def get_parser() -> ArgumentParser:
def calc_end_time(duration: Optional[float]) -> Optional[datetime]:
- """returns the time when a poll should it from hours"""
+ """returns the time when a poll should be closed"""
if duration != 0 and not None:
return utcnow() + relativedelta(hours=int(duration))
return
From 981076af846f2e5a8d9584594210129473490b71 Mon Sep 17 00:00:00 2001
From: NekoFanatic <83883849+NekoFanatic@users.noreply.github.com>
Date: Mon, 6 Jun 2022 20:51:44 +0200
Subject: [PATCH 36/44] some changes
---
general/polls/cog.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/general/polls/cog.py b/general/polls/cog.py
index c98dcdacc..d11cd90a6 100644
--- a/general/polls/cog.py
+++ b/general/polls/cog.py
@@ -356,7 +356,7 @@ class PollsCog(Cog, name="Polls"):
Contributor.Defelo,
Contributor.TNT2k,
Contributor.wolflu,
- Contributor.NekoFanatic, # rewrote most of this code (Please blame @Defelo for the code)
+ Contributor.NekoFanatic,
]
def __init__(self, team_roles: list[str]):
From ba3e73cccd18cc07dc7c60b515c49ac2245b5b2d Mon Sep 17 00:00:00 2001
From: NekoFanatic <83883849+NekoFanatic@users.noreply.github.com>
Date: Mon, 6 Jun 2022 21:01:57 +0200
Subject: [PATCH 37/44] some changes
---
general/polls/cog.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/general/polls/cog.py b/general/polls/cog.py
index d11cd90a6..32c2eed1c 100644
--- a/general/polls/cog.py
+++ b/general/polls/cog.py
@@ -571,7 +571,7 @@ async def duration(self, ctx: Context, hours: int = None):
@settings.command(name="max_duration", aliases=["md"])
@PollsPermission.write.check
@docs(t.commands.poll.settings.max_duration)
- async def max_duration(self, ctx: Context, days: int = None):
+ async def max_duration(self, ctx: Context, days: int | None = None):
days = days or 7
msg: str = t.max_duration.set(cnt=days)
@@ -582,7 +582,7 @@ async def max_duration(self, ctx: Context, days: int = None):
@settings.command(name="votes", aliases=["v", "choices", "c"])
@PollsPermission.write.check
@docs(t.commands.poll.settings.votes)
- async def votes(self, ctx: Context, votes: int = None):
+ async def votes(self, ctx: Context, votes: int | None = None):
if not votes:
votes = 0
msg: str = t.votes.reset
From e58f29ee4d5840ca8ddbf3e9f65eb86909ab1e5c Mon Sep 17 00:00:00 2001
From: NekoFanatic <83883849+NekoFanatic@users.noreply.github.com>
Date: Mon, 6 Jun 2022 21:32:10 +0200
Subject: [PATCH 38/44] some changes
---
general/polls/cog.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/general/polls/cog.py b/general/polls/cog.py
index 32c2eed1c..f87ae62ae 100644
--- a/general/polls/cog.py
+++ b/general/polls/cog.py
@@ -136,7 +136,7 @@ async def send_poll(
poll_args: str,
max_choices: int = None,
field: Optional[tuple[str, str]] = None,
- deadline: Optional[float] = None,
+ deadline: Optional[int] = None,
) -> tuple[Message, Message, list[tuple[str, str]], str]:
"""sends a poll embed + view message containing the select field"""
From 8e1428b599a258f1b7dcef9fa19410b1f171cfde Mon Sep 17 00:00:00 2001
From: Infinity <83883849+Inf-inity@users.noreply.github.com>
Date: Wed, 15 Jun 2022 19:03:42 +0200
Subject: [PATCH 39/44] Update cog.py
---
general/polls/cog.py | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/general/polls/cog.py b/general/polls/cog.py
index f87ae62ae..7a4dfdeba 100644
--- a/general/polls/cog.py
+++ b/general/polls/cog.py
@@ -427,11 +427,12 @@ async def poll_list(self, ctx: Context):
description += t.polls.row(
poll.title, poll.message_url, poll.owner_id, format_dt(poll.end_time, style="R")
)
- if description:
- embed: Embed = Embed(title=t.polls.title, description=description, color=Colors.Polls)
- await send_long_embed(ctx, embed=embed, paginate=True)
- if not polls or not description:
+ if polls and description:
+ embed: Embed = Embed(title=t.polls.title, description=description, color=Colors.Polls)
+ await send_long_embed(ctx, embed=embed, paginate=True)
+
+ else:
await send_long_embed(ctx, embed=Embed(title=t.no_polls, color=Colors.error))
@poll.command(name="delete", aliases=["del"])
From 705da8cfe5e7dd43f827b9b26a54035f6c8e7429 Mon Sep 17 00:00:00 2001
From: Inf-inity <83883849+Inf-inity@users.noreply.github.com>
Date: Wed, 15 Jun 2022 20:40:01 +0200
Subject: [PATCH 40/44] some changes
---
general/polls/cog.py | 9 ++++++++-
general/polls/translations/en.yml | 1 +
2 files changed, 9 insertions(+), 1 deletion(-)
diff --git a/general/polls/cog.py b/general/polls/cog.py
index 7a4dfdeba..a337464fe 100644
--- a/general/polls/cog.py
+++ b/general/polls/cog.py
@@ -237,6 +237,9 @@ async def get_staff(guild: Guild, team_roles: list[str]) -> set[Member]:
teamlers.update(member for member in team_role.members if not member.bot)
+ if not teamlers:
+ raise CommandError(t.error.no_teamlers)
+
return teamlers
@@ -332,7 +335,11 @@ async def callback(self, interaction):
)
if poll.poll_type == PollType.TEAM:
- teamlers: set[Member] = await get_staff(interaction.guild, ["team"])
+ try:
+ teamlers: set[Member] = await get_staff(interaction.guild, ["team"])
+ except CommandError:
+ await interaction.response.send_message(content=t.error.no_teamlers, ephemeral=True)
+ return
if user not in teamlers:
await interaction.response.send_message(content=t.team_yn_poll_forbidden, ephemeral=True)
return
diff --git a/general/polls/translations/en.yml b/general/polls/translations/en.yml
index d228f7512..fcbd97149 100644
--- a/general/polls/translations/en.yml
+++ b/general/polls/translations/en.yml
@@ -28,6 +28,7 @@ error:
weight_too_small: "Weight cant be lower than `0.1`"
cant_set_weight: Can't set weight!
not_poll: Mesage doesn't contains a poll
+ no_teamlers: No user with team-role found!
cant_pin:
title: Error
description: Can't pin any more messages in {}
From 926aedadf2d6df2e7e1f33651da61e60cfd10e8e Mon Sep 17 00:00:00 2001
From: Inf-inity <83883849+Inf-inity@users.noreply.github.com>
Date: Fri, 17 Jun 2022 20:39:47 +0200
Subject: [PATCH 41/44] some changes
---
general/polls/cog.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/general/polls/cog.py b/general/polls/cog.py
index a337464fe..6bfa32f76 100644
--- a/general/polls/cog.py
+++ b/general/polls/cog.py
@@ -125,7 +125,7 @@ async def get_parser() -> ArgumentParser:
def calc_end_time(duration: Optional[float]) -> Optional[datetime]:
"""returns the time when a poll should be closed"""
- if duration != 0 and not None:
+ if duration:
return utcnow() + relativedelta(hours=int(duration))
return
From b975ea1bfd4da18a4e985866d9dc58f66e862c55 Mon Sep 17 00:00:00 2001
From: Inf-inity <83883849+Inf-inity@users.noreply.github.com>
Date: Fri, 17 Jun 2022 20:48:56 +0200
Subject: [PATCH 42/44] some more moneyless changes
---
general/polls/cog.py | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/general/polls/cog.py b/general/polls/cog.py
index 6bfa32f76..ea306d1ef 100644
--- a/general/polls/cog.py
+++ b/general/polls/cog.py
@@ -125,9 +125,7 @@ async def get_parser() -> ArgumentParser:
def calc_end_time(duration: Optional[float]) -> Optional[datetime]:
"""returns the time when a poll should be closed"""
- if duration:
- return utcnow() + relativedelta(hours=int(duration))
- return
+ return utcnow() + relativedelta(hours=int(duration)) if duration else None
async def send_poll(
From 0f231be2720373adeda9e81946ab6be1a050ac93 Mon Sep 17 00:00:00 2001
From: Inf-inity <83883849+Inf-inity@users.noreply.github.com>
Date: Mon, 20 Jun 2022 21:56:36 +0200
Subject: [PATCH 43/44] this is a commit message
---
general/polls/cog.py | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/general/polls/cog.py b/general/polls/cog.py
index ea306d1ef..82f3350de 100644
--- a/general/polls/cog.py
+++ b/general/polls/cog.py
@@ -361,7 +361,7 @@ class PollsCog(Cog, name="Polls"):
Contributor.Defelo,
Contributor.TNT2k,
Contributor.wolflu,
- Contributor.NekoFanatic,
+ Contributor.Infinity,
]
def __init__(self, team_roles: list[str]):
@@ -485,7 +485,10 @@ async def voted(self, ctx: Context, message: Message):
users = {}
for option in poll.options:
for vote in option.votes:
- users[str(vote.user_id)] = users.get(str(vote.user_id), default=[]).append(option.field_position + 1)
+ if not users.get(str(vote.user_id)):
+ users[str(vote.user_id)] = [option.field_position + 1]
+ else:
+ users[str(vote.user_id)].append(option.field_position + 1)
description = ""
for key, value in users.items():
From 8f9609c86ea713b07b8d3c474e07179426efe78c Mon Sep 17 00:00:00 2001
From: Inf-inity <83883849+Inf-inity@users.noreply.github.com>
Date: Mon, 20 Jun 2022 22:07:54 +0200
Subject: [PATCH 44/44] resolved change requests
---
general/polls/cog.py | 4 ++--
general/polls/models.py | 3 +--
2 files changed, 3 insertions(+), 4 deletions(-)
diff --git a/general/polls/cog.py b/general/polls/cog.py
index 82f3350de..03bdc21f4 100644
--- a/general/polls/cog.py
+++ b/general/polls/cog.py
@@ -541,7 +541,7 @@ async def settings(self, ctx: Context):
@settings.command(name="roles_weights", aliases=["rw"])
@PollsPermission.write.check
@docs(t.commands.poll.settings.roles_weights)
- async def roles_weights(self, ctx: Context, role: Role, weight: float = None):
+ async def roles_weights(self, ctx: Context, role: Role, weight: float | None = None):
element = await db.get(RoleWeight, role_id=role.id)
if not weight and not element:
@@ -566,7 +566,7 @@ async def roles_weights(self, ctx: Context, role: Role, weight: float = None):
@settings.command(name="duration", aliases=["d"])
@PollsPermission.write.check
@docs(t.commands.poll.settings.duration)
- async def duration(self, ctx: Context, hours: int = None):
+ async def duration(self, ctx: Context, hours: int | None = None):
if not hours:
hours = 0
msg: str = t.duration.reset()
diff --git a/general/polls/models.py b/general/polls/models.py
index af0202cfd..7cadb11b7 100644
--- a/general/polls/models.py
+++ b/general/polls/models.py
@@ -30,8 +30,7 @@ async def sync_redis(role_id: int = None) -> list[dict[str, int | float]]:
await pipe.delete(key := f"poll_role_weight={role_id or weights.role_id}")
save = {"role": int(weights.role_id), "weight": float(weights.weight)}
out.append(save)
- await pipe.set(key, str(weights.weight))
- await pipe.expire(key, CACHE_TTL)
+ await pipe.setex(key, CACHE_TTL, str(weights.weight))
await pipe.execute()