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.
278 lines
9.7 KiB
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)
|
|
|