From a5749f095f091ee8af98d0a92abea8276d1001e4 Mon Sep 17 00:00:00 2001 From: Leandro Lucarella Date: Wed, 6 Aug 2008 14:15:51 -0300 Subject: [PATCH] Add basic comments support This patch implements the basics for comments support. Two new classes are added: Comment and CommentDB. Comments are stored in almost the same format as articles, but a comment DB is present for each article. All comments are stored in the ``comments_path`` directory (which usually won't be the same as the ``data_path`` because, when online comment posting is implemented, it will need to be writeable by the web server). Each article should have a subdirectory in that path (with the article's uuid as directory name), where a ``db`` file is expected, with this format:: comment number, creation time (epoch) Comments are numbered incrementally and this number is considered both the ID and the filename where the comment contents are stored under the article's comments directory. An empty line in the comments DB represents a deleted comment. Comment files have a similar format to Article files, a header is expected in the form of key, values: Author: Leandro Lucarella Link: http://www.example.com/ Headers end with an empty line, where the body begin, in RestructuredText format. The link can be any URL (for example, mailto:pomelo@example.com). A new attribute ``comments`` is added to Article class, with a list of comments for that article (loaded via the CommentDB class). Note that a race is possible if more than one process add a comment at the same time, because of how the CommentDB.save() method is implemented (the same happens with ArticleDB.save(), but it's more likely that 2 comments are added at the same, for example when online commenting is implemented). --- blitiri.cgi | 214 ++++++++++++++++++++++++++++++++++++++++++++++- config.py.sample | 3 + 2 files changed, 213 insertions(+), 4 deletions(-) diff --git a/blitiri.cgi b/blitiri.cgi index 0e2aea6..11a7df8 100755 --- a/blitiri.cgi +++ b/blitiri.cgi @@ -14,6 +14,9 @@ # Directory where entries are stored data_path = "/tmp/blog/data" +# Directory where comments are stored (must be writeable by the web server) +comments_path = "/tmp/blog/comments" + # Path where templates are stored. Use an empty string for the built-in # default templates. If they're not found, the built-in ones will be used. templates_path = "/tmp/blog/templates" @@ -114,7 +117,8 @@ default_article_header = """ %(umonth)02d-\ %(uday)02d\ %(uhour)02d:%(uminute)02d)
- tagged %(tags)s + tagged %(tags)s - + with %(comments)s comment(s)

@@ -126,6 +130,23 @@ default_article_footer = """
""" +default_comment_header = """ +
+ +

Comment #%(number)d

+by %(author)s + on %(year)04d-%(month)02d-%(day)02d %(hour)02d:%(minute)02d +

+

+""" + +default_comment_footer = """ +

+

+
+""" + + # Default CSS default_css = """ body { @@ -152,7 +173,14 @@ h2 { border-bottom: 1px solid #99C; } -h1 a, h2 a { +h3 { + font-size: small; + font-weigth: none; + margin-bottom: 1pt; + border-bottom: 1px solid #99C; +} + +h1 a, h2 a, h3 a { text-decoration: none; color: black; } @@ -179,6 +207,37 @@ div.article { margin-bottom: 2em; } +span.cominfo { + font-size: xx-small; +} + +span.cominfo a { + text-decoration: none; + color: #339; +} + +span.cominfo a:hover { + text-decoration: none; + color: blue; +} + +div.combody { + margin-left: 2em; +} + +div.comment { + margin-left: 1em; + margin-bottom: 1em; +} + +hr { + float: left; + height: 2px; + border: 0; + background-color: #99F; + width: 60%; +} + div.footer { margin-top: 1em; padding-top: 0.4em; @@ -277,6 +336,135 @@ class Templates (object): return self.get_template( 'art_footer', default_article_footer, article.to_vars()) + def get_comment_header(self, comment): + return self.get_template( + 'com_header', default_comment_header, comment.to_vars()) + + def get_comment_footer(self, comment): + return self.get_template( + 'com_footer', default_comment_footer, comment.to_vars()) + + +class Comment (object): + def __init__(self, article, number, created = None): + self.article = article + self.number = number + if created is None: + self.created = datetime.datetime.now() + else: + self.created = created + + self.loaded = False + + # loaded on demand + self._author = author + self._link = '' + self._raw_content = 'Removed comment' + + + def get_author(self): + if not self.loaded: + self.load() + return self._author + author = property(fget = get_author) + + def get_link(self): + if not self.loaded: + self.load() + return self._link + link = property(fget = get_link) + + def get_raw_content(self): + if not self.loaded: + self.load() + return self._raw_content + raw_content = property(fget = get_raw_content) + + + def load(self): + filename = os.path.join(comments_path, self.article.uuid, + str(self.number)) + try: + raw = open(filename).readlines() + except: + return + + count = 0 + for l in raw: + if ':' in l: + name, value = l.split(':', 1) + if name.lower() == 'author': + self._author = value.strip() + elif name.lower() == 'link': + self._link = value.strip() + elif l == '\n': + # end of header + break + count += 1 + self._raw_content = ''.join(raw[count + 1:]) + self.loaded = True + + def to_html(self): + return rst_to_html(self.raw_content) + + def to_vars(self): + return { + 'number': self.number, + 'author': sanitize(self.author), + 'link': sanitize(self.link), + 'date': self.created.isoformat(' '), + 'created': self.created.isoformat(' '), + + 'year': self.created.year, + 'month': self.created.month, + 'day': self.created.day, + 'hour': self.created.hour, + 'minute': self.created.minute, + 'second': self.created.second, + } + +class CommentDB (object): + def __init__(self, article): + self.path = os.path.join(comments_path, article.uuid) + self.comments = [] + self.load(article) + + def load(self, article): + try: + f = open(os.path.join(self.path, 'db')) + except: + return + + for l in f: + # Each line has the following comma separated format: + # number, created (epoch) + # Empty lines are meaningful and represent removed + # comments (so we can preserve the comment number) + l = l.split(',') + try: + n = int(l[0]) + d = datetime.datetime.fromtimestamp(float(l[1])) + except: + # Removed/invalid comment + self.comments.append(None) + continue + self.comments.append(Comment(article, n, d)) + + def save(self): + old_db = os.path.join(self.path, 'db') + new_db = os.path.join(self.path, 'db.tmp') + f = open(new_db, 'w') + for c in self.comments: + s = '' + if c is not None: + s = '' + s += str(c.number) + ', ' + s += str(time.mktime(c.created.timetuple())) + s += '\n' + f.write(s) + f.close() + os.rename(new_db, old_db) + class Article (object): def __init__(self, path, created = None, updated = None): @@ -292,6 +480,7 @@ class Article (object): self._author = author self._tags = [] self._raw_content = '' + self._comments = [] def get_title(self): @@ -318,6 +507,12 @@ class Article (object): return self._raw_content raw_content = property(fget = get_raw_content) + def get_comments(self): + if not self.loaded: + self.load() + return self._comments + comments = property(fget = get_comments) + def __cmp__(self, other): if self.path == other.path: @@ -363,6 +558,8 @@ class Article (object): break count += 1 self._raw_content = ''.join(raw[count + 1:]) + db = CommentDB(self) + self._comments = db.comments self.loaded = True def to_html(self): @@ -375,6 +572,7 @@ class Article (object): 'date': self.created.isoformat(' '), 'uuid': self.uuid, 'tags': self.get_tags_links(), + 'comments': len(self.comments), 'created': self.created.isoformat(' '), 'ciso': self.created.isoformat(), @@ -488,7 +686,7 @@ class ArticleDB (object): # -def render_html(articles, db, actyear = None): +def render_html(articles, db, actyear = None, show_comments = False): template = Templates(templates_path, db, actyear) print 'Content-type: text/html; charset=utf-8\n' print template.get_main_header() @@ -496,6 +694,14 @@ def render_html(articles, db, actyear = None): print template.get_article_header(a) print a.to_html() print template.get_article_footer(a) + if show_comments: + print '' + for c in a.comments: + if c is None: + continue + print template.get_comment_header(c) + print c.to_html() + print template.get_comment_footer(c) print template.get_main_footer() def render_artlist(articles, db, actyear = None): @@ -613,7 +819,7 @@ def handle_cgi(): elif style: render_style() elif post: - render_html( [db.get_article(uuid)], db, year ) + render_html( [db.get_article(uuid)], db, year, True ) elif artlist: articles = db.get_articles() articles.sort(cmp = Article.title_cmp) diff --git a/config.py.sample b/config.py.sample index b623358..f145a3e 100644 --- a/config.py.sample +++ b/config.py.sample @@ -10,6 +10,9 @@ # Directory where entries are stored data_path = "/tmp/blog/data" +# Directory where comments are stored (must be writeable by the web server) +comments_path = "/tmp/blog/comments" + # Path where templates are stored. Use an empty string for the built-in # default templates. If they're not found, the built-in ones will be used. templates_path = "/tmp/blog/templates" -- 2.43.0