Refactor, add very basic support for MoxQuizz questions.
- Add rudimentary classes for reading MoxQuizz questions. - Add a very simple ask command. No scores or quiz game-play yet. - Refactor SQL models out into a separate file. - Make the code more Python 3 friendly. - PEP8 and pyflakes.
This commit is contained in:
parent
4f546f18a3
commit
3100081986
3 changed files with 314 additions and 100 deletions
161
lolbot.py
161
lolbot.py
|
|
@ -9,6 +9,8 @@ Useful bot for folks stuck behind censor walls at work
|
||||||
Logs a channel and collects URLs for later.
|
Logs a channel and collects URLs for later.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from __future__ import print_function # unicode_literals
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
|
|
@ -21,12 +23,12 @@ try:
|
||||||
from twisted.internet import protocol
|
from twisted.internet import protocol
|
||||||
from twisted.internet import reactor
|
from twisted.internet import reactor
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from mechanize import Browser
|
from sqlalchemy import create_engine
|
||||||
from sqlalchemy import MetaData, Table, Column, String, Text, Integer, DateTime, create_engine
|
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
from pymoxquizz import QuestionBank, Question
|
||||||
|
from models import Url, Log, Model
|
||||||
except ImportError:
|
except ImportError:
|
||||||
print "Some modules missing: Lolbot relies on Twisted IRC, Mechanize and SQLAlchemy.\n"
|
print("Some modules missing: Lolbot relies on Twisted IRC, Mechanize and SQLAlchemy.\n")
|
||||||
sys.exit
|
sys.exit
|
||||||
|
|
||||||
# Exclamations - wrong input
|
# Exclamations - wrong input
|
||||||
|
|
@ -47,70 +49,6 @@ ponderings = [
|
||||||
"No it's a week night 8pm is past my bedtime.",
|
"No it's a week night 8pm is past my bedtime.",
|
||||||
]
|
]
|
||||||
|
|
||||||
SqlBase = declarative_base()
|
|
||||||
|
|
||||||
class Log(SqlBase):
|
|
||||||
"""
|
|
||||||
This class represents an event in the log table and inherits from a SQLAlchemy
|
|
||||||
convenience ORM class.
|
|
||||||
"""
|
|
||||||
|
|
||||||
__tablename__ = "log"
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
|
||||||
timestamp = Column(DateTime)
|
|
||||||
nickname = Column(String(20))
|
|
||||||
text = Column(Text)
|
|
||||||
|
|
||||||
def __init__(self, nickname, text, timestamp=None):
|
|
||||||
"""
|
|
||||||
Creates an event log for the IRC logger.
|
|
||||||
"""
|
|
||||||
if timestamp is None:
|
|
||||||
timestamp = datetime.now()
|
|
||||||
self.timestamp = timestamp
|
|
||||||
self.nickname = nickname
|
|
||||||
self.text = text
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return "(%s) %s: %s" % (self.timestamp.strftime("%Y-%m-%d %H:%M:%S"), self.nickname, self.text)
|
|
||||||
|
|
||||||
class Url(SqlBase):
|
|
||||||
"""
|
|
||||||
This class represents a saved URL and inherits from a SQLAlchemy convenience
|
|
||||||
ORM class.
|
|
||||||
"""
|
|
||||||
|
|
||||||
__tablename__ = "url"
|
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
|
||||||
timestamp = Column(DateTime)
|
|
||||||
nickname = Column(String(20))
|
|
||||||
url = Column(String(200), unique=True)
|
|
||||||
title = Column(Text)
|
|
||||||
|
|
||||||
def __init__(self, nickname, url, title=None, timestamp=None):
|
|
||||||
if timestamp is None:
|
|
||||||
timestamp = datetime.now()
|
|
||||||
self.timestamp = timestamp
|
|
||||||
self.nickname = nickname
|
|
||||||
self.url = url
|
|
||||||
self.title = title
|
|
||||||
|
|
||||||
# populate the title from the URL if not given.
|
|
||||||
if title is None:
|
|
||||||
try:
|
|
||||||
br = Browser()
|
|
||||||
br.open(self.url)
|
|
||||||
self.title = br.title()
|
|
||||||
except Exception as ex:
|
|
||||||
self.title = ''
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
if not self.title:
|
|
||||||
return "%s: %s" % (self.nickname, self.url)
|
|
||||||
else:
|
|
||||||
return "%s: %s - %s" % (self.nickname, self.url, self.title)
|
|
||||||
|
|
||||||
class LolBot(irc.IRCClient):
|
class LolBot(irc.IRCClient):
|
||||||
"""
|
"""
|
||||||
|
|
@ -123,10 +61,15 @@ class LolBot(irc.IRCClient):
|
||||||
return connection
|
return connection
|
||||||
|
|
||||||
def created(self, when):
|
def created(self, when):
|
||||||
|
# load some MoxQuizz questions
|
||||||
|
self.qb = QuestionBank('/home/johnno/questions.doctorlard.en').questions
|
||||||
|
random.shuffle(self.qb)
|
||||||
|
self.question = None
|
||||||
|
|
||||||
# connect to the database
|
# connect to the database
|
||||||
self.dbengine = create_engine('sqlite+pysqlite://', creator=self._get_connection)
|
self.dbengine = create_engine('sqlite+pysqlite://', creator=self._get_connection)
|
||||||
SqlBase.metadata.bind = self.dbengine
|
Model.metadata.bind = self.dbengine
|
||||||
SqlBase.metadata.create_all()
|
Model.metadata.create_all()
|
||||||
self.get_session = sessionmaker(bind=self.dbengine)
|
self.get_session = sessionmaker(bind=self.dbengine)
|
||||||
|
|
||||||
self.helptext = "Keeps a list of URLs. Commands: list [n|x-y] - prints the last 10 URLs (or n URLs, or x through y); clear - clears the list; lol - say something funny; <url> - adds the URL to the list; help - this message."
|
self.helptext = "Keeps a list of URLs. Commands: list [n|x-y] - prints the last 10 URLs (or n URLs, or x through y); clear - clears the list; lol - say something funny; <url> - adds the URL to the list; help - this message."
|
||||||
|
|
@ -144,10 +87,10 @@ class LolBot(irc.IRCClient):
|
||||||
db.commit()
|
db.commit()
|
||||||
else:
|
else:
|
||||||
theurl = db.query(Url).filter(Url.url == url).one()
|
theurl = db.query(Url).filter(Url.url == url).one()
|
||||||
print theurl
|
print(theurl)
|
||||||
title = theurl.title
|
title = theurl.title
|
||||||
except Exception, ex:
|
except Exception as ex:
|
||||||
print "Exception caught saving URL: %s" % ex
|
print("Exception caught saving URL: %s" % ex)
|
||||||
return title
|
return title
|
||||||
|
|
||||||
def log_event(self, nick, text):
|
def log_event(self, nick, text):
|
||||||
|
|
@ -156,9 +99,9 @@ class LolBot(irc.IRCClient):
|
||||||
db = self.get_session()
|
db = self.get_session()
|
||||||
db.add(entry)
|
db.add(entry)
|
||||||
db.commit()
|
db.commit()
|
||||||
print entry
|
print(entry)
|
||||||
except Exception, ex:
|
except Exception as ex:
|
||||||
print "Exception caught logging event: %s" % ex
|
print("Exception caught logging event: %s" % ex)
|
||||||
|
|
||||||
def _get_nickname(self):
|
def _get_nickname(self):
|
||||||
return self.factory.nickname
|
return self.factory.nickname
|
||||||
|
|
@ -174,13 +117,14 @@ class LolBot(irc.IRCClient):
|
||||||
|
|
||||||
def signedOn(self):
|
def signedOn(self):
|
||||||
self.join(self.channel)
|
self.join(self.channel)
|
||||||
print "Signed on as %s." % (self.nickname,)
|
print("Signed on as %s." % (self.nickname,))
|
||||||
|
|
||||||
def joined(self, channel):
|
def joined(self, channel):
|
||||||
print "Joined %s." % (channel,)
|
print("Joined %s." % (channel,))
|
||||||
|
|
||||||
def privmsg(self, user, channel, msg):
|
def privmsg(self, user, channel, msg):
|
||||||
user = user.split('!')[0]
|
user = user.split('!')[0]
|
||||||
|
|
||||||
if channel == self.nickname:
|
if channel == self.nickname:
|
||||||
# Private /msg from a user
|
# Private /msg from a user
|
||||||
self.do_command(msg, user)
|
self.do_command(msg, user)
|
||||||
|
|
@ -188,6 +132,15 @@ class LolBot(irc.IRCClient):
|
||||||
# log it
|
# log it
|
||||||
self.log_event(user, msg)
|
self.log_event(user, msg)
|
||||||
|
|
||||||
|
# deal with MoxQuizz question
|
||||||
|
answered = False
|
||||||
|
if isinstance(self.question, Question) and self.question.answer.lower() in msg.lower():
|
||||||
|
answered = True
|
||||||
|
print("%s +1" % user)
|
||||||
|
self.reply('Correct! %s scores one point.' % user)
|
||||||
|
if answered:
|
||||||
|
self.question = None
|
||||||
|
|
||||||
args = string.split(msg, ":", 1)
|
args = string.split(msg, ":", 1)
|
||||||
if len(args) > 1 and args[0] == self.nickname:
|
if len(args) > 1 and args[0] == self.nickname:
|
||||||
self.do_command(string.strip(args[1]))
|
self.do_command(string.strip(args[1]))
|
||||||
|
|
@ -197,7 +150,7 @@ class LolBot(irc.IRCClient):
|
||||||
for w in words:
|
for w in words:
|
||||||
if w.startswith('http://') or w.startswith('https://'):
|
if w.startswith('http://') or w.startswith('https://'):
|
||||||
title = self.save_url(user, w)
|
title = self.save_url(user, w)
|
||||||
if title == False:
|
if title is False:
|
||||||
self.say_public("Sorry, I'm useless at UTF-8.")
|
self.say_public("Sorry, I'm useless at UTF-8.")
|
||||||
else:
|
else:
|
||||||
self.say_public("URL added. %s" % title)
|
self.say_public("URL added. %s" % title)
|
||||||
|
|
@ -236,6 +189,10 @@ class LolBot(irc.IRCClient):
|
||||||
if cmd == 'help':
|
if cmd == 'help':
|
||||||
self.reply(self.helptext, target)
|
self.reply(self.helptext, target)
|
||||||
|
|
||||||
|
elif cmd == 'ask':
|
||||||
|
self.question = random.choice(self.qb)
|
||||||
|
self.reply(str(self.question.question))
|
||||||
|
|
||||||
elif cmd == 'lol':
|
elif cmd == 'lol':
|
||||||
self.reply(self.ponder(), target)
|
self.reply(self.ponder(), target)
|
||||||
|
|
||||||
|
|
@ -259,14 +216,14 @@ class LolBot(irc.IRCClient):
|
||||||
y = abs(int(y))
|
y = abs(int(y))
|
||||||
if y < x:
|
if y < x:
|
||||||
x, y = y, x
|
x, y = y, x
|
||||||
except ValueError, ex:
|
except ValueError as ex:
|
||||||
self.reply("Give me a number or a range of numbers, e.g. list 5 or list 11-20", target)
|
self.reply("Give me a number or a range of numbers, e.g. list 5 or list 11-20", target)
|
||||||
raise ex
|
raise ex
|
||||||
rows = db.query(Url).order_by(Url.timestamp.desc())[x-1:y]
|
rows = db.query(Url).order_by(Url.timestamp.desc())[x-1:y]
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
n = abs(int(n))
|
n = abs(int(n))
|
||||||
except ValueError, ex:
|
except ValueError as ex:
|
||||||
self.reply("Give me a number or a range of numbers, e.g. list 5 or list 11-20", target)
|
self.reply("Give me a number or a range of numbers, e.g. list 5 or list 11-20", target)
|
||||||
raise ex
|
raise ex
|
||||||
rows = db.query(Url).order_by(Url.timestamp.desc())[:n]
|
rows = db.query(Url).order_by(Url.timestamp.desc())[:n]
|
||||||
|
|
@ -277,8 +234,8 @@ class LolBot(irc.IRCClient):
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
elif cmd.startswith('http:') or cmd.startswith('https:'):
|
elif cmd.startswith('http:') or cmd.startswith('https:'):
|
||||||
title = self.save_url(from_private, cmd)
|
title = self.save_url(target, cmd)
|
||||||
if title == False:
|
if title is False:
|
||||||
self.say_public("Sorry, I'm useless at UTF-8.")
|
self.say_public("Sorry, I'm useless at UTF-8.")
|
||||||
else:
|
else:
|
||||||
self.reply('URL added. %s' % title, target)
|
self.reply('URL added. %s' % title, target)
|
||||||
|
|
@ -286,12 +243,13 @@ class LolBot(irc.IRCClient):
|
||||||
else:
|
else:
|
||||||
self.reply(self.exclaim(), target)
|
self.reply(self.exclaim(), target)
|
||||||
|
|
||||||
except Exception, ex:
|
except Exception as ex:
|
||||||
print "Exception caught processing command: %s" % ex
|
print("Exception caught processing command: %s" % ex)
|
||||||
print " command was '%s' from %s" % (cmd, target)
|
print(" command was '%s' from %s" % (cmd, target))
|
||||||
self.reply("Sorry, I didn't understand: %s" % cmd, target)
|
self.reply("Sorry, I didn't understand: %s" % cmd, target)
|
||||||
self.reply(self.helptext, target)
|
self.reply(self.helptext, target)
|
||||||
|
|
||||||
|
|
||||||
class LolBotFactory(protocol.ClientFactory):
|
class LolBotFactory(protocol.ClientFactory):
|
||||||
protocol = LolBot
|
protocol = LolBot
|
||||||
|
|
||||||
|
|
@ -303,28 +261,30 @@ class LolBotFactory(protocol.ClientFactory):
|
||||||
self.channel = self.config['irc.channel']
|
self.channel = self.config['irc.channel']
|
||||||
|
|
||||||
def clientConnectionLost(self, connector, reason):
|
def clientConnectionLost(self, connector, reason):
|
||||||
print "Lost connection (%s), reconnecting." % (reason,)
|
print("Lost connection (%s), reconnecting." % (reason,))
|
||||||
connector.connect()
|
connector.connect()
|
||||||
|
|
||||||
def clientConnectionFailed(self, connector, reason):
|
def clientConnectionFailed(self, connector, reason):
|
||||||
print "Could not connect: %s" % (reason,)
|
print("Could not connect: %s" % (reason,))
|
||||||
|
|
||||||
|
|
||||||
def get_options():
|
def get_options():
|
||||||
try:
|
try:
|
||||||
(options, args) = getopt.getopt(sys.argv[1:], "hc:", ["help", "config=", ])
|
(options, args) = getopt.getopt(sys.argv[1:], "hc:", ["help", "config=", ])
|
||||||
except getopt.GetoptError, err:
|
except getopt.GetoptError as err:
|
||||||
print str(err)
|
print(str(err))
|
||||||
usage()
|
usage()
|
||||||
sys.exit(2)
|
sys.exit(2)
|
||||||
|
|
||||||
config_path = ""
|
config_path = ""
|
||||||
for option,value in options:
|
for option, value in options:
|
||||||
if option in ("-h", "--help"):
|
if option in ("-h", "--help"):
|
||||||
usage()
|
usage()
|
||||||
sys.exit(2)
|
sys.exit(2)
|
||||||
if option in ("-c", "--config"):
|
if option in ("-c", "--config"):
|
||||||
config_path = value
|
config_path = value
|
||||||
return { 'config_path': config_path }
|
return {'config_path': config_path}
|
||||||
|
|
||||||
|
|
||||||
def get_config(config_path):
|
def get_config(config_path):
|
||||||
"""
|
"""
|
||||||
|
|
@ -342,7 +302,7 @@ def get_config(config_path):
|
||||||
if not config_path:
|
if not config_path:
|
||||||
config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "lolbot.conf")
|
config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "lolbot.conf")
|
||||||
if not os.path.exists(config_path):
|
if not os.path.exists(config_path):
|
||||||
print "Error: configuration file not found. By default lolbot will look for a lolbot.conf file in the same directory as the lolbot script, but you can override this by specifying a path on the command line with the --config option."
|
print("Error: configuration file not found. By default lolbot will look for a lolbot.conf file in the same directory as the lolbot script, but you can override this by specifying a path on the command line with the --config option.")
|
||||||
usage()
|
usage()
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
@ -364,7 +324,7 @@ def get_config(config_path):
|
||||||
|
|
||||||
# validate IRC host
|
# validate IRC host
|
||||||
if "irc.server" not in config.keys():
|
if "irc.server" not in config.keys():
|
||||||
print "Error: the IRC server was not specified. Use --help for more information."
|
print("Error: the IRC server was not specified. Use --help for more information.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# validate IRC port
|
# validate IRC port
|
||||||
|
|
@ -373,12 +333,12 @@ def get_config(config_path):
|
||||||
try:
|
try:
|
||||||
config["irc.port"] = int(config["irc.port"])
|
config["irc.port"] = int(config["irc.port"])
|
||||||
except ValueError:
|
except ValueError:
|
||||||
print "Error: the IRC port must be an integer. If not specified, lolbot will use the default IRC port value 6667. Use --help for more information."
|
print("Error: the IRC port must be an integer. If not specified, lolbot will use the default IRC port value 6667. Use --help for more information.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# validate IRC channel
|
# validate IRC channel
|
||||||
if "irc.channel" not in config.keys() or not config["irc.channel"].startswith("#"):
|
if "irc.channel" not in config.keys() or not config["irc.channel"].startswith("#"):
|
||||||
print "Error: the IRC channel is not specified or incorrect. It must begin with a # - e.g. #mychatchannel. Use --help for more information."
|
print("Error: the IRC channel is not specified or incorrect. It must begin with a # - e.g. #mychatchannel. Use --help for more information.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# validate bot nickname
|
# validate bot nickname
|
||||||
|
|
@ -387,8 +347,9 @@ def get_config(config_path):
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
def usage():
|
def usage():
|
||||||
print """Run a lolbot.
|
print("""Run a lolbot.
|
||||||
|
|
||||||
-h, --help
|
-h, --help
|
||||||
This message.
|
This message.
|
||||||
|
|
@ -416,7 +377,8 @@ Configuration:
|
||||||
by SQL Alchemy. If not specified, lolbot will attempt to use a
|
by SQL Alchemy. If not specified, lolbot will attempt to use a
|
||||||
SQLite database called lolbot.db in the same directory as the
|
SQLite database called lolbot.db in the same directory as the
|
||||||
script.
|
script.
|
||||||
"""
|
""")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
args = get_options()
|
args = get_options()
|
||||||
|
|
@ -425,5 +387,4 @@ if __name__ == "__main__":
|
||||||
reactor.connectTCP(config['irc.server'], config['irc.port'], LolBotFactory(args['config_path']))
|
reactor.connectTCP(config['irc.server'], config['irc.port'], LolBotFactory(args['config_path']))
|
||||||
reactor.run()
|
reactor.run()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print "Shutting down."
|
print("Shutting down.")
|
||||||
|
|
||||||
|
|
|
||||||
72
models.py
Normal file
72
models.py
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
from mechanize import Browser
|
||||||
|
from datetime import datetime
|
||||||
|
from sqlalchemy import (Column, String, Text, Integer, DateTime)
|
||||||
|
from sqlalchemy.ext.declarative import (declarative_base)
|
||||||
|
|
||||||
|
|
||||||
|
Model = declarative_base()
|
||||||
|
|
||||||
|
|
||||||
|
class Log(Model):
|
||||||
|
"""
|
||||||
|
This class represents an event in the log table and inherits from a SQLAlchemy
|
||||||
|
convenience ORM class.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__ = "log"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
timestamp = Column(DateTime)
|
||||||
|
nickname = Column(String(20))
|
||||||
|
text = Column(Text)
|
||||||
|
|
||||||
|
def __init__(self, nickname, text, timestamp=None):
|
||||||
|
"""
|
||||||
|
Creates an event log for the IRC logger.
|
||||||
|
"""
|
||||||
|
if timestamp is None:
|
||||||
|
timestamp = datetime.now()
|
||||||
|
self.timestamp = timestamp
|
||||||
|
self.nickname = nickname
|
||||||
|
self.text = text
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "(%s) %s: %s" % (self.timestamp.strftime("%Y-%m-%d %H:%M:%S"), self.nickname, self.text)
|
||||||
|
|
||||||
|
|
||||||
|
class Url(Model):
|
||||||
|
"""
|
||||||
|
This class represents a saved URL and inherits from a SQLAlchemy convenience
|
||||||
|
ORM class.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__ = "url"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
timestamp = Column(DateTime)
|
||||||
|
nickname = Column(String(20))
|
||||||
|
url = Column(String(200), unique=True)
|
||||||
|
title = Column(Text)
|
||||||
|
|
||||||
|
def __init__(self, nickname, url, title=None, timestamp=None):
|
||||||
|
if timestamp is None:
|
||||||
|
timestamp = datetime.now()
|
||||||
|
self.timestamp = timestamp
|
||||||
|
self.nickname = nickname
|
||||||
|
self.url = url
|
||||||
|
self.title = title
|
||||||
|
|
||||||
|
# populate the title from the URL if not given.
|
||||||
|
if title is None:
|
||||||
|
try:
|
||||||
|
br = Browser()
|
||||||
|
br.open(self.url)
|
||||||
|
self.title = br.title()
|
||||||
|
except Exception:
|
||||||
|
self.title = ''
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
if not self.title:
|
||||||
|
return "%s: %s" % (self.nickname, self.url)
|
||||||
|
else:
|
||||||
|
return "%s: %s - %s" % (self.nickname, self.url, self.title)
|
||||||
181
pymoxquizz.py
Normal file
181
pymoxquizz.py
Normal file
|
|
@ -0,0 +1,181 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
## -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals, print_function
|
||||||
|
from io import open
|
||||||
|
|
||||||
|
|
||||||
|
class Question:
|
||||||
|
"""
|
||||||
|
Represents one MoxQuizz question.
|
||||||
|
"""
|
||||||
|
|
||||||
|
category = None
|
||||||
|
question = None
|
||||||
|
answer = None
|
||||||
|
regexp = None
|
||||||
|
author = None
|
||||||
|
level = None
|
||||||
|
comment = None
|
||||||
|
score = 0
|
||||||
|
tip = list()
|
||||||
|
tipcycle = 0
|
||||||
|
|
||||||
|
TRIVIAL = 1
|
||||||
|
EASY = 2
|
||||||
|
NORMAL = 3
|
||||||
|
HARD = 4
|
||||||
|
EXTREME = 5
|
||||||
|
|
||||||
|
LEVELS = (TRIVIAL, EASY, NORMAL, HARD, EXTREME)
|
||||||
|
|
||||||
|
def __init__(self, attributes_dict):
|
||||||
|
self.parse(attributes_dict)
|
||||||
|
|
||||||
|
def parse(self, attributes_dict):
|
||||||
|
"""
|
||||||
|
Populate fields from a dictionary of attributes (from a question bank).
|
||||||
|
"""
|
||||||
|
|
||||||
|
## Valid keys:
|
||||||
|
# ----------
|
||||||
|
# Category? (should always be on top!)
|
||||||
|
# Question (should always stand after Category)
|
||||||
|
# Answer (will be matched if no regexp is provided)
|
||||||
|
# Regexp? (use UNIX-style expressions)
|
||||||
|
# Author? (the brain behind this question)
|
||||||
|
# Level? [baby|easy|normal|hard|extreme] (difficulty)
|
||||||
|
# Comment? (comment line)
|
||||||
|
# Score? [#] (credits for answering this question)
|
||||||
|
# Tip* (provide one or more hints)
|
||||||
|
# TipCycle? [#] (Specify number of generated tips)
|
||||||
|
|
||||||
|
if 'Question' in attributes_dict.keys():
|
||||||
|
self.question = attributes_dict['Question']
|
||||||
|
else:
|
||||||
|
raise Exception("Cannot instantiate Question: 'Question' attribute required.")
|
||||||
|
|
||||||
|
if 'Category' in attributes_dict.keys():
|
||||||
|
self.category = attributes_dict['Category']
|
||||||
|
|
||||||
|
if 'Answer' in attributes_dict.keys():
|
||||||
|
self.answer = attributes_dict['Answer']
|
||||||
|
else:
|
||||||
|
raise Exception("Cannot instantiate Question: 'Answer' attribute required.")
|
||||||
|
|
||||||
|
if 'Regexp' in attributes_dict.keys():
|
||||||
|
self.regexp = attributes_dict['Regexp']
|
||||||
|
|
||||||
|
if 'Author' in attributes_dict.keys():
|
||||||
|
self.category = attributes_dict['Author']
|
||||||
|
|
||||||
|
if 'Level' in attributes_dict.keys() and attributes_dict['Level'] in self.LEVELS:
|
||||||
|
self.level = attributes_dict['level']
|
||||||
|
|
||||||
|
if 'Comment' in attributes_dict.keys():
|
||||||
|
self.comment = attributes_dict['Comment']
|
||||||
|
|
||||||
|
if 'Score' in attributes_dict.keys():
|
||||||
|
self.score = attributes_dict['Score']
|
||||||
|
|
||||||
|
if 'Tip' in attributes_dict.keys():
|
||||||
|
self.tip = attributes_dict['Tip']
|
||||||
|
|
||||||
|
if 'Tipcycle' in attributes_dict.keys():
|
||||||
|
self.tipcycle = attributes_dict['Tipcycle']
|
||||||
|
|
||||||
|
|
||||||
|
class QuestionBank:
|
||||||
|
"""
|
||||||
|
Represents a MoxQuizz question bank.
|
||||||
|
"""
|
||||||
|
|
||||||
|
filename = ''
|
||||||
|
questions = list()
|
||||||
|
|
||||||
|
# Case sensitive, to remain backwards-compatible with MoxQuizz.
|
||||||
|
KEYS = ('Answer',
|
||||||
|
'Author',
|
||||||
|
'Category',
|
||||||
|
'Comment',
|
||||||
|
'Level',
|
||||||
|
'Question',
|
||||||
|
'Regexp',
|
||||||
|
'Score',
|
||||||
|
'Tip',
|
||||||
|
'Tipcycle',
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, filename):
|
||||||
|
"""
|
||||||
|
Construct a question bank from a file.
|
||||||
|
"""
|
||||||
|
self.filename = filename
|
||||||
|
self.questions = self.parse(filename)
|
||||||
|
|
||||||
|
def parse(self, filename):
|
||||||
|
"""
|
||||||
|
Read a Moxquizz question bank file into a list.
|
||||||
|
"""
|
||||||
|
questions = list()
|
||||||
|
|
||||||
|
with open(filename) as f:
|
||||||
|
key = ''
|
||||||
|
i = 0
|
||||||
|
|
||||||
|
# new question
|
||||||
|
q = dict()
|
||||||
|
q['Tip'] = list()
|
||||||
|
|
||||||
|
for line in f:
|
||||||
|
line = line.strip()
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
# Ignore comments.
|
||||||
|
if line.startswith('#'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# A blank line starts a new question.
|
||||||
|
if line == '':
|
||||||
|
# Store the previous question, if valid.
|
||||||
|
if 'Question' in q.keys() and 'Answer' in q.keys():
|
||||||
|
question = Question(q)
|
||||||
|
questions.append(question)
|
||||||
|
|
||||||
|
# Start a new question.
|
||||||
|
q = dict()
|
||||||
|
q['Tip'] = list()
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Fetch the next parameter.
|
||||||
|
try:
|
||||||
|
(key, value) = line.split(':', 1)
|
||||||
|
except ValueError:
|
||||||
|
print("Unexpected weirdness in MoxQuizz questionbank '%s', line %s." % (self.filename, i))
|
||||||
|
continue
|
||||||
|
# break # TODO: is it appropriate to bail on broken bank files?
|
||||||
|
|
||||||
|
# Ignore bad parameters.
|
||||||
|
if key not in self.KEYS:
|
||||||
|
print("Unexpected key '%s' in MoxQuizz questionbank '%s', line %s." % (key, self.filename, i))
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Enumerate the Tips.
|
||||||
|
if key == 'Tip':
|
||||||
|
q['Tip'].append(value.strip())
|
||||||
|
else:
|
||||||
|
q[key] = value.strip()
|
||||||
|
|
||||||
|
return questions
|
||||||
|
|
||||||
|
|
||||||
|
# A crappy test.
|
||||||
|
if __name__ == '__main__':
|
||||||
|
qb = QuestionBank('questions.doctorlard.en')
|
||||||
|
for q in qb.questions:
|
||||||
|
print(q.question)
|
||||||
|
a = unicode(raw_input('A: '), 'utf8')
|
||||||
|
#a = input('A: ') # Python 3
|
||||||
|
if a.lower() == q.answer.lower():
|
||||||
|
print("Correct!")
|
||||||
|
else:
|
||||||
|
print("Incorrect - the answer is '%s'" % q.answer)
|
||||||
Loading…
Add table
Reference in a new issue