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