mirror of
https://github.com/Mikaela/Limnoria.git
synced 2025-01-24 02:54:05 +01:00
Poll: Initial commit with basic features.
This commit is contained in:
parent
3b25a94b46
commit
d919e2133d
@ -121,8 +121,9 @@ class Autocomplete(callbacks.Plugin):
|
||||
"""Provides command completion for IRC clients that support it."""
|
||||
|
||||
def _enabled(self, irc, msg):
|
||||
return conf.supybot.protocols.irc.experimentalExtensions() and self.registryValue(
|
||||
"enabled", msg.channel, irc.network
|
||||
return (
|
||||
conf.supybot.protocols.irc.experimentalExtensions()
|
||||
and self.registryValue("enabled", msg.channel, irc.network)
|
||||
)
|
||||
|
||||
def doTagmsg(self, irc, msg):
|
||||
|
58
plugins/Poll/README.rst
Normal file
58
plugins/Poll/README.rst
Normal file
@ -0,0 +1,58 @@
|
||||
.. _plugin-Poll:
|
||||
|
||||
Documentation for the Poll plugin for Supybot
|
||||
=============================================
|
||||
|
||||
Purpose
|
||||
-------
|
||||
Poll: Provides a simple way to vote on answers to a question
|
||||
|
||||
Usage
|
||||
-----
|
||||
Provides a simple way to vote on answers to a question
|
||||
|
||||
.. _commands-Poll:
|
||||
|
||||
Commands
|
||||
--------
|
||||
.. _command-poll-add:
|
||||
|
||||
add [<channel>] <question> <answer1> [<answer2> [<answer3> [...]]]
|
||||
Creates a new poll with the specified <question> and answers on the <channel>. The first word of each answer is used as its id to vote, so each answer should start with a different word. <channel> is only necessary if this command is run in private, and defaults to the current channel otherwise.
|
||||
|
||||
.. _command-poll-close:
|
||||
|
||||
close [<channel>] <poll_id>
|
||||
Closes the specified poll.
|
||||
|
||||
.. _command-poll-results:
|
||||
|
||||
results [<channel>] <poll_id>
|
||||
Returns the results of the specified poll.
|
||||
|
||||
.. _command-poll-vote:
|
||||
|
||||
vote [<channel>] <poll_id> <answer_id>
|
||||
Registers your vote on the poll <poll_id> as being the answer identified by <answer_id> (which is the first word of each possible answer).
|
||||
|
||||
.. _conf-Poll:
|
||||
|
||||
Configuration
|
||||
-------------
|
||||
|
||||
.. _conf-supybot.plugins.Poll.public:
|
||||
|
||||
|
||||
supybot.plugins.Poll.public
|
||||
This config variable defaults to "True", is not network-specific, and is not channel-specific.
|
||||
|
||||
Determines whether this plugin is publicly visible.
|
||||
|
||||
.. _conf-supybot.plugins.Poll.requireManageCapability:
|
||||
|
||||
|
||||
supybot.plugins.Poll.requireManageCapability
|
||||
This config variable defaults to "channel,op; channel,halfop", is network-specific, and is channel-specific.
|
||||
|
||||
Determines the capabilities required (if any) to open and close polls. Use 'channel,capab' for channel-level capabilities. Note that absence of an explicit anticapability means user has capability.
|
||||
|
72
plugins/Poll/__init__.py
Normal file
72
plugins/Poll/__init__.py
Normal file
@ -0,0 +1,72 @@
|
||||
###
|
||||
# Copyright (c) 2021, Valentin Lorentz
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions, and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions, and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
# * Neither the name of the author of this software nor the name of
|
||||
# contributors to this software may be used to endorse or promote products
|
||||
# derived from this software without specific prior written consent.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
###
|
||||
|
||||
"""
|
||||
Poll: Provides a simple way to vote on answers to a question
|
||||
"""
|
||||
|
||||
import sys
|
||||
import supybot
|
||||
from supybot import world
|
||||
|
||||
# Use this for the version of this plugin.
|
||||
__version__ = ""
|
||||
|
||||
# XXX Replace this with an appropriate author or supybot.Author instance.
|
||||
__author__ = supybot.authors.unknown
|
||||
|
||||
# This is a dictionary mapping supybot.Author instances to lists of
|
||||
# contributions.
|
||||
__contributors__ = {}
|
||||
|
||||
# This is a url where the most recent plugin package can be downloaded.
|
||||
__url__ = ""
|
||||
|
||||
from . import config
|
||||
from . import plugin
|
||||
|
||||
if sys.version_info >= (3, 4):
|
||||
from importlib import reload
|
||||
else:
|
||||
from imp import reload
|
||||
# In case we're being reloaded.
|
||||
reload(config)
|
||||
reload(plugin)
|
||||
# Add more reloads here if you add third-party modules and want them to be
|
||||
# reloaded when this plugin is reloaded. Don't forget to import them as well!
|
||||
|
||||
if world.testing:
|
||||
from . import test
|
||||
|
||||
Class = plugin.Class
|
||||
configure = config.configure
|
||||
|
||||
|
||||
# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79:
|
72
plugins/Poll/config.py
Normal file
72
plugins/Poll/config.py
Normal file
@ -0,0 +1,72 @@
|
||||
###
|
||||
# Copyright (c) 2021, Valentin Lorentz
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions, and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions, and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
# * Neither the name of the author of this software nor the name of
|
||||
# contributors to this software may be used to endorse or promote products
|
||||
# derived from this software without specific prior written consent.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
###
|
||||
|
||||
from supybot import conf, registry
|
||||
|
||||
try:
|
||||
from supybot.i18n import PluginInternationalization
|
||||
|
||||
_ = PluginInternationalization("Poll")
|
||||
except:
|
||||
# Placeholder that allows to run the plugin on a bot
|
||||
# without the i18n module
|
||||
_ = lambda x: x
|
||||
|
||||
|
||||
def configure(advanced):
|
||||
# This will be called by supybot to configure this module. advanced is
|
||||
# a bool that specifies whether the user identified themself as an advanced
|
||||
# user or not. You should effect your configuration by manipulating the
|
||||
# registry as appropriate.
|
||||
from supybot.questions import expect, anything, something, yn
|
||||
|
||||
conf.registerPlugin("Poll", True)
|
||||
|
||||
|
||||
Poll = conf.registerPlugin("Poll")
|
||||
# This is where your configuration variables (if any) should go. For example:
|
||||
# conf.registerGlobalValue(Poll, 'someConfigVariableName',
|
||||
# registry.Boolean(False, _("""Help for someConfigVariableName.""")))
|
||||
|
||||
conf.registerChannelValue(
|
||||
Poll,
|
||||
"requireManageCapability",
|
||||
registry.String(
|
||||
"channel,op; channel,halfop",
|
||||
_(
|
||||
"""Determines the capabilities required (if any) to open and
|
||||
close polls.
|
||||
Use 'channel,capab' for channel-level capabilities.
|
||||
Note that absence of an explicit anticapability means user has
|
||||
capability.
|
||||
"""
|
||||
),
|
||||
),
|
||||
)
|
1
plugins/Poll/local/__init__.py
Normal file
1
plugins/Poll/local/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# Stub so local is a module, used for third-party modules
|
191
plugins/Poll/plugin.py
Normal file
191
plugins/Poll/plugin.py
Normal file
@ -0,0 +1,191 @@
|
||||
###
|
||||
# Copyright (c) 2021, Valentin Lorentz
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions, and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions, and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
# * Neither the name of the author of this software nor the name of
|
||||
# contributors to this software may be used to endorse or promote products
|
||||
# derived from this software without specific prior written consent.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
###
|
||||
|
||||
import collections
|
||||
import re
|
||||
|
||||
from supybot import utils, plugins, ircdb, ircutils, callbacks
|
||||
from supybot.commands import *
|
||||
|
||||
try:
|
||||
from supybot.i18n import PluginInternationalization
|
||||
|
||||
_ = PluginInternationalization("Poll")
|
||||
except ImportError:
|
||||
# Placeholder that allows to run the plugin on a bot
|
||||
# without the i18n module
|
||||
_ = lambda x: x
|
||||
|
||||
|
||||
Poll = collections.namedtuple("Poll", "question answers votes open")
|
||||
|
||||
|
||||
class Poll_(callbacks.Plugin):
|
||||
"""Provides a simple way to vote on answers to a question"""
|
||||
|
||||
def __init__(self, irc):
|
||||
super().__init__(irc)
|
||||
|
||||
# {(network, channel): {id: Poll}}
|
||||
self._polls = collections.defaultdict(dict)
|
||||
|
||||
def name(self):
|
||||
return "Poll"
|
||||
|
||||
def _checkManageCapability(self, irc, msg, channel):
|
||||
# Copy-pasted from Topic
|
||||
capabilities = self.registryValue(
|
||||
"requireManageCapability", channel, irc.network
|
||||
)
|
||||
for capability in re.split(r"\s*;\s*", capabilities):
|
||||
if capability.startswith("channel,"):
|
||||
capability = ircdb.makeChannelCapability(
|
||||
channel, capability[8:]
|
||||
)
|
||||
if capability and ircdb.checkCapability(msg.prefix, capability):
|
||||
return
|
||||
irc.errorNoCapability(capabilities, Raise=True)
|
||||
|
||||
def _getPoll(self, irc, channel, poll_id):
|
||||
poll = self._polls[(irc.network, channel)].get(poll_id)
|
||||
if poll is None:
|
||||
irc.error(
|
||||
_("A poll with this ID does not exist in this channel."),
|
||||
Raise=True,
|
||||
)
|
||||
return poll
|
||||
|
||||
@wrap(["channel", "something", many("something")])
|
||||
def add(self, irc, msg, args, channel, question, answers):
|
||||
"""[<channel>] <question> <answer1> [<answer2> [<answer3> [...]]]
|
||||
|
||||
Creates a new poll with the specified <question> and answers
|
||||
on the <channel>.
|
||||
The first word of each answer is used as its id to vote,
|
||||
so each answer should start with a different word.
|
||||
|
||||
<channel> is only necessary if this command is run in private,
|
||||
and defaults to the current channel otherwise."""
|
||||
self._checkManageCapability(irc, msg, channel)
|
||||
|
||||
poll_id = max(self._polls[(irc.network, channel)], default=0) + 1
|
||||
|
||||
answers = [(answer.split()[0], answer) for answer in answers]
|
||||
|
||||
answer_id_counts = collections.Counter(id_ for (id_, _) in answers).items()
|
||||
duplicate_answer_ids = [
|
||||
answer_id for (answer_id, count) in answer_id_counts if count > 1
|
||||
]
|
||||
if duplicate_answer_ids:
|
||||
irc.error(
|
||||
format(
|
||||
_("Duplicate answer identifier(s): %L"), duplicate_answer_ids
|
||||
),
|
||||
Raise=True,
|
||||
)
|
||||
|
||||
self._polls[(irc.network, channel)][poll_id] = Poll(
|
||||
question=question, answers=dict(answers), votes={}, open=True
|
||||
)
|
||||
|
||||
irc.replySuccess(_("Poll # %d created.") % poll_id)
|
||||
|
||||
@wrap(["channel", "nonNegativeInt"])
|
||||
def close(self, irc, msg, args, channel, poll_id):
|
||||
"""[<channel>] <poll_id>
|
||||
|
||||
Closes the specified poll."""
|
||||
self._checkManageCapability(irc, msg, channel)
|
||||
|
||||
poll = self._getPoll(irc, channel, poll_id)
|
||||
|
||||
if not poll.open:
|
||||
irc.error(_("This poll was already closed."), Raise=True)
|
||||
|
||||
poll = Poll(
|
||||
question=poll.question,
|
||||
answers=poll.answers,
|
||||
votes=poll.votes,
|
||||
open=False,
|
||||
)
|
||||
self._polls[(irc.network, channel)][poll_id] = poll
|
||||
irc.replySuccess()
|
||||
|
||||
@wrap(["channel", "nonNegativeInt", "somethingWithoutSpaces"])
|
||||
def vote(self, irc, msg, args, channel, poll_id, answer_id):
|
||||
"""[<channel>] <poll_id> <answer_id>
|
||||
|
||||
Registers your vote on the poll <poll_id> as being the answer
|
||||
identified by <answer_id> (which is the first word of each possible
|
||||
answer)."""
|
||||
|
||||
poll = self._getPoll(irc, channel, poll_id)
|
||||
|
||||
if not poll.open:
|
||||
irc.error(_("This poll is closed."), Raise=True)
|
||||
|
||||
if msg.nick in poll.votes:
|
||||
irc.error(_("You already voted on this poll."), Raise=True)
|
||||
|
||||
if answer_id not in poll.answers:
|
||||
irc.error(
|
||||
format(
|
||||
_("Invalid answer ID. Valid answers are: %L"),
|
||||
poll.answers,
|
||||
),
|
||||
Raise=True,
|
||||
)
|
||||
|
||||
poll.votes[msg.nick] = answer_id
|
||||
|
||||
irc.replySuccess()
|
||||
|
||||
@wrap(["channel", "nonNegativeInt"])
|
||||
def results(self, irc, msg, args, channel, poll_id):
|
||||
"""[<channel>] <poll_id>
|
||||
|
||||
Returns the results of the specified poll."""
|
||||
|
||||
poll = self._getPoll(irc, channel, poll_id)
|
||||
|
||||
counts = collections.Counter(poll.votes.values())
|
||||
|
||||
# Add answers with 0 votes
|
||||
counts.update({answer_id: 0 for answer_id in poll.answers})
|
||||
|
||||
results = [
|
||||
format(_("%n for %s"), (v, "vote"), k)
|
||||
for (k, v) in counts.most_common()
|
||||
]
|
||||
|
||||
irc.replies(results)
|
||||
|
||||
|
||||
Class = Poll_
|
124
plugins/Poll/test.py
Normal file
124
plugins/Poll/test.py
Normal file
@ -0,0 +1,124 @@
|
||||
###
|
||||
# Copyright (c) 2021, Valentin Lorentz
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions, and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions, and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
# * Neither the name of the author of this software nor the name of
|
||||
# contributors to this software may be used to endorse or promote products
|
||||
# derived from this software without specific prior written consent.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
###
|
||||
|
||||
from supybot.test import *
|
||||
|
||||
|
||||
class PollTestCase(ChannelPluginTestCase):
|
||||
plugins = ("Poll",)
|
||||
|
||||
def testBasics(self):
|
||||
self.assertResponse(
|
||||
'poll add "Is this a test?" "Yes" "No" "Maybe"',
|
||||
"The operation succeeded. Poll # 1 created.",
|
||||
)
|
||||
|
||||
self.assertNotError("vote 1 Yes", frm="voter1!foo@bar")
|
||||
self.assertNotError("vote 1 No", frm="voter2!foo@bar")
|
||||
self.assertNotError("vote 1 No", frm="voter3!foo@bar")
|
||||
|
||||
self.assertResponse(
|
||||
"results 1",
|
||||
"2 votes for No, 1 vote for Yes, and 0 votes for Maybe",
|
||||
)
|
||||
|
||||
def testDoubleVoting(self):
|
||||
self.assertResponse(
|
||||
'poll add "Is this a test?" "Yes" "No" "Maybe"',
|
||||
"The operation succeeded. Poll # 1 created.",
|
||||
)
|
||||
|
||||
self.assertNotError("vote 1 Yes", frm="voter1!foo@bar")
|
||||
self.assertNotError("vote 1 No", frm="voter2!foo@bar")
|
||||
self.assertResponse(
|
||||
"vote 1 Yes",
|
||||
"voter1: Error: You already voted on this poll.",
|
||||
frm="voter1!foo@bar",
|
||||
)
|
||||
|
||||
self.assertRegexp(
|
||||
"results 1",
|
||||
"1 vote for (Yes|No), 1 vote for (Yes|No), and 0 votes for Maybe",
|
||||
)
|
||||
|
||||
def testClosed(self):
|
||||
self.assertResponse(
|
||||
'poll add "Is this a test?" "Yes" "No" "Maybe"',
|
||||
"The operation succeeded. Poll # 1 created.",
|
||||
)
|
||||
|
||||
self.assertNotError("vote 1 Yes", frm="voter1!foo@bar")
|
||||
self.assertNotError("vote 1 No", frm="voter2!foo@bar")
|
||||
self.assertNotError("close 1")
|
||||
self.assertResponse(
|
||||
"vote 1 Yes",
|
||||
"voter3: Error: This poll is closed.",
|
||||
frm="voter3!foo@bar",
|
||||
)
|
||||
self.assertRegexp("close 1", "already closed")
|
||||
|
||||
self.assertRegexp(
|
||||
"results 1",
|
||||
"1 vote for (Yes|No), 1 vote for (Yes|No), and 0 votes for Maybe",
|
||||
)
|
||||
|
||||
def testNonExisting(self):
|
||||
self.assertResponse(
|
||||
'poll add "Is this a test?" "Yes" "No" "Maybe"',
|
||||
"The operation succeeded. Poll # 1 created.",
|
||||
)
|
||||
|
||||
self.assertRegexp("vote 2 Yes", "does not exist")
|
||||
|
||||
def testLongAnswers(self):
|
||||
self.assertResponse(
|
||||
'poll add "Is this a test?" "Yes totally" "No no no" "Maybe"',
|
||||
"The operation succeeded. Poll # 1 created.",
|
||||
)
|
||||
|
||||
self.assertNotError("vote 1 Yes", frm="voter1!foo@bar")
|
||||
self.assertNotError("vote 1 No", frm="voter2!foo@bar")
|
||||
self.assertNotError("vote 1 No", frm="voter3!foo@bar")
|
||||
|
||||
self.assertResponse(
|
||||
"results 1",
|
||||
"2 votes for No, 1 vote for Yes, and 0 votes for Maybe",
|
||||
)
|
||||
|
||||
def testDuplicateId(self):
|
||||
self.assertResponse(
|
||||
'poll add "Is this a test?" "Yes" "Yes" "Maybe"',
|
||||
"Error: Duplicate answer identifier(s): Yes",
|
||||
)
|
||||
|
||||
self.assertResponse(
|
||||
'poll add "Is this a test?" "Yes totally" "Yes and no" "Maybe"',
|
||||
"Error: Duplicate answer identifier(s): Yes",
|
||||
)
|
@ -1,4 +1,4 @@
|
||||
[tool.black]
|
||||
line-length = 79
|
||||
|
||||
include = 'plugins/(Autocomplete|Fediverse)/.*\.pyi?$'
|
||||
include = 'plugins/(Autocomplete|Fediverse|Poll)/.*\.pyi?$'
|
||||
|
@ -84,7 +84,7 @@ class PluginDoc(object):
|
||||
def __init__(self, mod, titleTemplate):
|
||||
self.mod = mod
|
||||
self.inst = self.mod.Class(None)
|
||||
self.name = self.mod.Class.__name__
|
||||
self.name = self.inst.name()
|
||||
self.appendExtraBlankLine = False
|
||||
self.titleTemplate = string.Template(titleTemplate)
|
||||
self.lines = []
|
||||
|
Loading…
Reference in New Issue
Block a user