3
# Copyright (c) 2003-2005, Stéphan Kochen
6
# Redistribution and use in source and binary forms, with or without
7
# modification, are permitted provided that the following conditions are met:
9
# * Redistributions of source code must retain the above copyright notice,
10
# this list of conditions, and the following disclaimer.
11
# * Redistributions in binary form must reproduce the above copyright notice,
12
# this list of conditions, and the following disclaimer in the
13
# documentation and/or other materials provided with the distribution.
14
# * Neither the name of the author of this software nor the name of
15
# contributors to this software may be used to endorse or promote products
16
# derived from this software without specific prior written consent.
18
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
22
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
23
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
24
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
26
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28
# POSSIBILITY OF SUCH DAMAGE.
34
import supybot.ircmsgs as ircmsgs
35
import supybot.dbi as dbi
36
import supybot.conf as conf
37
import supybot.ircdb as ircdb
38
import supybot.utils as utils
39
from supybot.commands import *
40
import supybot.plugins as plugins
41
import supybot.ircutils as ircutils
42
import supybot.callbacks as callbacks
44
class PollError(Exception):
47
class OptionRecord(dbi.Record):
53
return format('#%i: %q', self.id, self.text)
55
class PollRecord(dbi.Record):
63
user = plugins.getUserName(self.by)
65
options = format('Options: %s', '; '.join(map(str, self.options)))
67
options = 'The poll has no options, yet'
72
return format('Poll #%i: %q started by %s. %s. Poll is %s.',
73
self.id, self.question, user, options, status)
75
class SqlitePollDB(object):
76
def __init__(self, filename):
77
self.dbs = ircutils.IrcDict()
78
self.filename = filename
81
for db in self.dbs.itervalues():
84
def _getDb(self, channel):
88
raise callbacks.Error, 'You need to have PySQLite installed to ' \
89
'use Poll. Download it at ' \
90
'<http://pysqlite.org/>'
91
filename = plugins.makeChannelFilename(self.filename, channel)
92
if filename in self.dbs:
93
return self.dbs[filename]
94
if os.path.exists(filename):
95
self.dbs[filename] = sqlite.connect(filename)
96
return self.dbs[filename]
97
db = sqlite.connect(filename)
98
self.dbs[filename] = db
100
cursor.execute("""CREATE TABLE polls (
101
id INTEGER PRIMARY KEY,
102
question TEXT UNIQUE ON CONFLICT IGNORE,
105
cursor.execute("""CREATE TABLE options (
109
UNIQUE (poll_id, id) ON CONFLICT IGNORE)""")
110
cursor.execute("""CREATE TABLE votes (
114
UNIQUE (user_id, poll_id)
115
ON CONFLICT IGNORE)""")
119
def get(self, channel, poll_id):
120
db = self._getDb(channel)
122
cursor.execute("""SELECT question, started_by, open
123
FROM polls WHERE id=%s""", poll_id)
125
(question, by, status) = cursor.fetchone()
127
raise dbi.NoRecordError
128
cursor.execute("""SELECT id, option FROM options WHERE poll_id=%s""",
131
options = [OptionRecord(i, text=o, votes=0)
132
for (i, o) in cursor.fetchall()]
135
return PollRecord(poll_id, question=question, status=status, by=by,
138
def open(self, channel, user, question):
139
db = self._getDb(channel)
141
cursor.execute("""INSERT INTO polls VALUES (NULL, %s, %s, 1)""",
144
cursor.execute("""SELECT id FROM polls WHERE question=%s""", question)
145
return cursor.fetchone()[0]
147
def closePoll(self, channel, id):
148
db = self._getDb(channel)
150
# Check to make sure that the poll exists
151
cursor.execute("""SELECT id FROM polls WHERE id=%s""", id)
152
if cursor.rowcount == 0:
153
raise dbi.NoRecordError
154
cursor.execute("""UPDATE polls SET open=0 WHERE id=%s""", id)
157
def add(self, channel, user, id, option):
158
db = self._getDb(channel)
160
# Only the poll starter or an admin can add options
161
cursor.execute("""SELECT started_by FROM polls
164
if cursor.rowcount == 0:
165
raise dbi.NoRecordError
166
if not ((user.id == cursor.fetchone()[0]) or
167
(ircdb.checkCapability(user.id, 'admin'))):
169
'That poll isn\'t yours and you aren\'t an admin.'
170
# and NOBODY can add options once a poll has votes
171
cursor.execute("""SELECT COUNT(user_id) FROM votes
174
if int(cursor.fetchone()[0]) != 0:
175
raise PollError, 'Cannot add options to a poll with votes.'
176
# Get the next highest id
177
cursor.execute("""SELECT MAX(id)+1 FROM options
180
option_id = cursor.fetchone()[0] or 1
181
cursor.execute("""INSERT INTO options VALUES
183
option_id, id, option)
186
def vote(self, channel, user, id, option):
187
db = self._getDb(channel)
189
cursor.execute("""SELECT open
190
FROM polls WHERE id=%s""",
192
if cursor.rowcount == 0:
193
raise dbi.NoRecordError
194
elif int(cursor.fetchone()[0]) == 0:
195
raise PollError, 'That poll is closed.'
196
cursor.execute("""SELECT id FROM options
200
if cursor.rowcount == 0:
201
raise PollError, 'There is no such option.'
202
cursor.execute("""SELECT option_id FROM votes
203
WHERE user_id=%s AND poll_id=%s""",
205
if cursor.rowcount == 0:
206
cursor.execute("""INSERT INTO votes VALUES (%s, %s, %s)""",
209
cursor.execute("""UPDATE votes SET option_id=%s
210
WHERE user_id=%s AND poll_id=%s""",
214
def results(self, channel, poll_id):
215
db = self._getDb(channel)
217
cursor.execute("""SELECT id, question, started_by, open
218
FROM polls WHERE id=%s""",
220
if cursor.rowcount == 0:
221
raise dbi.NoRecordError
222
(id, question, by, status) = cursor.fetchone()
223
by = plugins.getUserName(by)
224
cursor.execute("""SELECT count(user_id), option_id
229
SELECT 0, id AS option_id
233
SELECT option_id FROM votes
236
ORDER BY count(user_id) DESC""",
237
poll_id, poll_id, poll_id)
238
if cursor.rowcount == 0:
239
raise PollError, 'This poll has no votes yet.'
242
for count, option_id in cursor.fetchall():
243
cursor.execute("""SELECT option FROM options
244
WHERE id=%s AND poll_id=%s""",
246
option = cursor.fetchone()[0]
247
options.append(OptionRecord(option_id, votes=int(count),
249
return PollRecord(poll_id, question=question, status=status, by=by,
252
def select(self, channel):
253
db = self._getDb(channel)
255
cursor.execute("""SELECT id, started_by, question
259
return [PollRecord(id, question=q, by=by, status=1)
260
for (id, by, q) in cursor.fetchall()]
262
raise dbi.NoRecordError
264
PollDB = plugins.DB('Poll', {'sqlite': SqlitePollDB})
266
class Poll(callbacks.Plugin):
267
def __init__(self, irc):
268
self.__parent = super(Poll, self)
269
self.__parent.__init__(irc)
276
def poll(self, irc, msg, args, channel, id):
279
Displays the poll question and options for the given poll id.
280
<channel> is only necessary if the message isn't sent in the channel
284
record = self.db.get(channel, id)
285
except dbi.NoRecordError:
286
irc.error(format('There is no poll with id %i.', id), Raise=True)
288
poll = wrap(poll, ['channeldb', 'id'])
290
def open(self, irc, msg, args, channel, user, question):
291
"""[<channel>] <question>
293
Creates a new poll with the given question. <channel> is only
294
necessary if the message isn't sent in the channel itself.
296
irc.replySuccess(format('(poll #%i added)',
297
self.db.open(channel, user, question)))
298
open = wrap(open, ['channeldb', 'user', 'text'])
300
def close(self, irc, msg, args, channel, id):
303
Closes the poll with the given <id>; further votes will not be allowed.
304
<channel> is only necessary if the message isn't sent in the channel
308
self.db.closePoll(channel, id)
310
except dbi.NoRecordError:
311
irc.errorInvalid('poll id')
312
close = wrap(close, ['channeldb', ('id', 'poll')])
314
def add(self, irc, msg, args, channel, user, id, option):
315
"""[<channel>] <id> <option text>
317
Add an option with the given text to the poll with the given id.
318
<channel> is only necessary if the message isn't sent in the channel
322
self.db.add(channel, user, id, option)
324
except dbi.NoRecordError:
325
irc.errorInvalid('poll id')
328
add = wrap(add, ['channeldb', 'user', ('id', 'poll'), 'text'])
330
def vote(self, irc, msg, args, channel, user, id, option):
331
"""[<channel>] <poll id> <option id>
333
Vote for the option with the given id on the poll with the given poll
334
id. This command can also be used to override any previous vote.
335
<channel> is only necesssary if the message isn't sent in the channel
339
self.db.vote(channel, user, id, option)
341
except dbi.NoRecordError:
342
irc.errorInvalid('poll id')
345
vote = wrap(vote, ['channeldb', 'user', ('id', 'poll'), ('id', 'option')])
347
def results(self, irc, msg, args, channel, id):
348
""": Используйте !vote в привате, для получения информации по голосованию """
350
poll = self.db.results(channel, id)
351
reply = format('Результаты для голосования #%i: %q by %s',
352
poll.id, poll.question, poll.by)
353
options = poll.options
355
for option in options:
356
L.append(format('#%i %q = %i', option.id, option.text, option.votes))
357
reply += format(' - %L', L)
359
irc.queueMsg(ircmsgs.privmsg(msg.nick, reply))
360
except dbi.NoRecordError:
361
irc.error('There is no such poll.', Raise=True)
364
results = wrap(results, ['channeldb', ('id', 'poll')])
366
def list(self, irc, msg, args, channel):
369
Lists the currently open polls for <channel>. <channel> is only
370
necessary if the message isn't sent in the channel itself.
373
polls = self.db.select(channel)
374
polls = [format('#%i: %q', p.id, p.question)
376
irc.reply(format('%L', polls))
377
except dbi.NoRecordError:
378
irc.reply('This channel currently has no open polls.')
379
list = wrap(list, ['channeldb'])
385
# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: