You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
isso/isso/db.py

278 lines
9.7 KiB

# -*- encoding: utf-8 -*-
#
# Copyright 2012, Martin Zimmermann <info@posativ.org>. All rights reserved.
# License: BSD Style, 2 clauses. see isso/__init__.py
import abc
import time
import sqlite3
from isso.utils import Bloomfilter
from isso.models import Comment
class Abstract:
__metaclass__ = abc.ABCMeta
@abc.abstractmethod
def __init__(self, dbpath, moderation):
return
@abc.abstractmethod
def add(self, path, comment):
"""Add a new comment to the database. Returns a Comment object."""
return
@abc.abstractmethod
def activate(self, path, id):
"""Activate comment id if pending and return comment for (path, id)."""
return
@abc.abstractmethod
def update(self, path, id, comment):
"""
Update an existing comment, but only writeable fields such as text,
author, email, website and parent. This method should set the modified
field to the current time.
"""
return
@abc.abstractmethod
def delete(self, path, id):
"""
Delete a comment. There are two distinctions: a comment is referenced
by another valid comment's parent attribute or stand-a-lone. In this
case the comment can't be removed without losing depending comments.
Hence, delete removes all visible data such as text, author, email,
website sets the mode field to 4.
In the second case this comment can be safely removed without any side
effects."""
return
@abc.abstractmethod
def retrieve(self, path, mode):
"""
Return all comments for `path` with `mode`.
"""
return
@abc.abstractmethod
def recent(self, mode=7, limit=None):
"""
Return most recent comments with `mode`. If `limit` is None, return
*all* comments that are currently stored, otherwise limit by `limit`.
"""
return
@abc.abstractmethod
def like(self, path, id, remote_addr):
"""+1 a given comment. Returns the new like count (may not change because
the creater can't vote on his/her own comment and multiple votes from the
same ip address are ignored as well)."""
return
@abc.abstractmethod
def count(self, path=None, mode=1):
"""return count of comments for path (optional) and mode (defaults to
visible, not deleted or moderated comments)."""
return
class Threads(object):
def __init__(self, dbpath):
self.dbpath = dbpath
with sqlite3.connect(self.dbpath) as con:
sql = ('main.threads (path VARCHAR(255) NOT NULL, title TEXT'
'PRIMARY KEY path)')
con.execute("CREATE TABLE IF NOT EXISTS %s;" % sql)
def get(self, path):
with sqlite3.connect(self.dbpath) as con:
rv = con.execute("SELECT title FROM threads WHERE path=?", (path,)).fetchone()
if rv is not None:
return rv[0]
return None
def add(self, path, title):
with sqlite3.connect(self.dbpath) as con:
con.execute("INSERT INTO threads (path, title) VALUES (?, ?)", (path, title))
return title
class SQLite(Abstract):
"""A basic :class:`Abstract` implementation using SQLite3. All comments
share a single database. The tuple (id, path) acts as unique identifier
for a comment. Multiple comments per path (= that is the URI to your blog
post) are ordered by that id."""
fields = [
'path', 'id', 'created', 'modified',
'text', 'author', 'hash', 'website', 'parent', 'mode', 'voters'
]
def __init__(self, dbpath, moderation):
self.dbpath = dbpath
self.mode = 2 if moderation else 1
with sqlite3.connect(self.dbpath) as con:
sql = ('main.comments (path VARCHAR(255) NOT NULL, id INTEGER NOT NULL,'
'created FLOAT NOT NULL, modified FLOAT, text VARCHAR,'
'author VARCHAR(64), hash VARCHAR(32), website VARCHAR(64),'
'parent INTEGER, mode INTEGER,'
'likes INTEGER DEFAULT 0, dislikes INTEGER DEFAULT 0, voters BLOB NOT NULL,'
'PRIMARY KEY (id, path))')
con.execute("CREATE TABLE IF NOT EXISTS %s;" % sql)
# increment id if (id, path) is no longer unique
con.execute("""\
CREATE TRIGGER IF NOT EXISTS increment AFTER INSERT ON comments
BEGIN
UPDATE comments SET
id=(SELECT MAX(id)+1 FROM comments WHERE path=NEW.path)
WHERE rowid=NEW.rowid;
END;""")
self.threads = Threads(self.dbpath)
def query2comment(self, query):
if query is None:
return None
return Comment(
text=query[4], author=query[5], hash=query[6], website=query[7], parent=query[8],
path=query[0], id=query[1], created=query[2], modified=query[3], mode=query[9],
votes=query[10]
)
def add(self, uri, c, remote_addr):
voters = buffer(Bloomfilter(iterable=[remote_addr]).array)
with sqlite3.connect(self.dbpath) as con:
keys = ','.join(self.fields)
values = ','.join('?' * len(self.fields))
con.execute('INSERT INTO comments (%s) VALUES (%s);' % (keys, values), (
uri, 0, c['created'] or time.time(), None, c["text"], c["author"],
c["hash"], c["website"], c["parent"], self.mode, voters)
)
with sqlite3.connect(self.dbpath) as con:
return self.query2comment(
con.execute('SELECT *, MAX(id) FROM comments WHERE path=?;', (uri, )).fetchone())
def activate(self, path, id):
with sqlite3.connect(self.dbpath) as con:
con.execute("UPDATE comments SET mode=1 WHERE path=? AND id=? AND mode=2", (path, id))
return self.get(path, id)
def update(self, path, id, values):
with sqlite3.connect(self.dbpath) as con:
for key, value in values.iteritems():
con.execute('UPDATE comments SET %s=? WHERE path=? AND id=?;' % key,
(value, path, id))
with sqlite3.connect(self.dbpath) as con:
con.execute('UPDATE comments SET modified=? WHERE path=? AND id=?',
(time.time(), path, id))
return self.get(path, id)
def get(self, path, id):
with sqlite3.connect(self.dbpath) as con:
return self.query2comment(con.execute(
'SELECT * FROM comments WHERE path=? AND id=?;', (path, id)).fetchone())
def _remove_stale(self, con, path):
sql = ('DELETE FROM',
' comments',
'WHERE',
' path=? AND mode=4 AND id NOT IN (',
' SELECT',
' parent',
' FROM',
' comments',
' WHERE path=? AND parent IS NOT NULL)')
while con.execute(' '.join(sql), (path, path)).rowcount:
continue
def delete(self, path, id):
with sqlite3.connect(self.dbpath) as con:
sql = 'SELECT * FROM comments WHERE path=? AND parent=?'
refs = con.execute(sql, (path, id)).fetchone()
if refs is None:
con.execute('DELETE FROM comments WHERE path=? AND id=?', (path, id))
self._remove_stale(con, path)
return None
con.execute('UPDATE comments SET text=? WHERE path=? AND id=?', ('', path, id))
con.execute('UPDATE comments SET mode=? WHERE path=? AND id=?', (4, path, id))
for field in ('author', 'website'):
con.execute('UPDATE comments SET %s=? WHERE path=? AND id=?' % field,
(None, path, id))
self._remove_stale(con, path)
return self.get(path, id)
def like(self, path, id, remote_addr):
with sqlite3.connect(self.dbpath) as con:
rv = con.execute("SELECT likes, dislikes, voters FROM comments" \
+ " WHERE path=? AND id=?", (path, id)).fetchone()
likes, dislikes, voters = rv
if likes + dislikes >= 142:
return likes
bf = Bloomfilter(bytearray(voters), likes + dislikes)
if remote_addr in bf:
return likes
bf.add(remote_addr)
with sqlite3.connect(self.dbpath) as con:
con.execute("UPDATE comments SET likes = likes + 1 WHERE path=? AND id=?", (path, id))
con.execute("UPDATE comments SET voters = ? WHERE path=? AND id=?", (
buffer(bf.array), path, id))
return likes + 1
def count(self, path=None, mode=1):
if path is not None:
with sqlite3.connect(self.dbpath) as con:
return con.execute("SELECT COUNT(*) FROM comments WHERE path=?" \
+ " AND (? | mode) = ?", (path, mode, mode)).fetchone()
with sqlite3.connect(self.dbpath) as con:
return con.execute("SELECT COUNT(*) FROM comments WHERE" \
+ "(? | mode) = ?", (path, mode, mode)).fetchone()
def retrieve(self, path, mode=5):
with sqlite3.connect(self.dbpath) as con:
rv = con.execute("SELECT * FROM comments WHERE path=? AND (? | mode) = ?" \
+ " ORDER BY id ASC;", (path, mode, mode)).fetchall()
for item in rv:
yield self.query2comment(item)
def recent(self, mode=7, limit=None):
sql = 'SELECT * FROM comments WHERE (? | mode) = ? ORDER BY created DESC'
args = [mode, mode]
if limit:
sql += ' LIMIT ?'
args.append(limit)
with sqlite3.connect(self.dbpath) as con:
rv = con.execute(sql + ';', args).fetchall()
for item in rv:
yield self.query2comment(item)