]> git.llucax.com Git - software/blitiri.git/blobdiff - blitiri.cgi
Add basic comments support
[software/blitiri.git] / blitiri.cgi
index 75e1a0c8ec7cdef7fe84d39e755fdbacf40247e0..11a7df8f4628018a37e15a12ad6732bfe439efb9 100755 (executable)
@@ -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 = """
 <a class="date" href="%(url)s/%(uyear)d/%(umonth)d/">%(umonth)02d</a>-\
 <a class="date" href="%(url)s/%(uyear)d/%(umonth)d/%(uday)d/">%(uday)02d</a>\
     %(uhour)02d:%(uminute)02d)</span><br/>
-  <span class="tags">tagged %(tags)s</span>
+  <span class="tags">tagged %(tags)s</span> -
+  <span class="comments">with %(comments)s comment(s)</span>
 </span><br/>
 <p/>
 <div class="artbody">
@@ -126,6 +130,23 @@ default_article_footer = """
 </div>
 """
 
+default_comment_header = """
+<div class="comment">
+<a name="comment-%(number)d" />
+<h3><a href="#comment-%(number)d">Comment #%(number)d</a></h3>
+<span class="cominfo">by <a href="%(link)s">%(author)s</a>
+  on %(year)04d-%(month)02d-%(day)02d %(hour)02d:%(minute)02d</span>
+<p/>
+<div class="combody">
+"""
+
+default_comment_footer = """
+<p/>
+</div>
+</div>
+"""
+
+
 # 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;
@@ -202,7 +261,7 @@ div.section h1 {
 
 """
 
-# helper function
+# helper functions
 def rst_to_html(rst):
        settings = {
                'input_encoding': encoding,
@@ -212,6 +271,12 @@ def rst_to_html(rst):
                                writer_name = "html")
        return parts['body'].encode('utf8')
 
+def sanitize(obj):
+       if isinstance(obj, basestring):
+               return cgi.escape(obj, True)
+       return obj
+
+
 # find out our URL, needed for syndication
 try:
        n = os.environ['SERVER_NAME']
@@ -271,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):
@@ -286,6 +480,7 @@ class Article (object):
                self._author = author
                self._tags = []
                self._raw_content = ''
+               self._comments = []
 
 
        def get_title(self):
@@ -312,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:
@@ -329,8 +530,14 @@ class Article (object):
 
 
        def load(self):
+               # XXX this tweak is only needed for old DB format, where
+               # article's paths started with a slash
+               path = self.path
+               if path.startswith('/'):
+                       path = path[1:]
+               filename = os.path.join(data_path, path)
                try:
-                       raw = open(data_path + '/' + self.path).readlines()
+                       raw = open(filename).readlines()
                except:
                        return
 
@@ -351,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):
@@ -358,11 +567,12 @@ class Article (object):
 
        def to_vars(self):
                return {
-                       'arttitle': self.title,
-                       'author': self.author,
+                       'arttitle': sanitize(self.title),
+                       'author': sanitize(self.author),
                        'date': self.created.isoformat(' '),
                        'uuid': self.uuid,
                        'tags': self.get_tags_links(),
+                       'comments': len(self.comments),
 
                        'created': self.created.isoformat(' '),
                        'ciso': self.created.isoformat(),
@@ -389,11 +599,11 @@ class Article (object):
                tags.sort()
                for t in tags:
                        l.append('<a class="tag" href="%s/tag/%s">%s</a>' % \
-                               (blog_url, urllib.quote(t), t) )
+                               (blog_url, urllib.quote(t), sanitize(t) ))
                return ', '.join(l)
 
 
-class DB (object):
+class ArticleDB (object):
        def __init__(self, dbpath):
                self.dbpath = dbpath
                self.articles = []
@@ -476,7 +686,7 @@ class DB (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()
@@ -484,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 '<a name="comments" />'
+                       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):
@@ -593,7 +811,7 @@ def handle_cgi():
                        t = urllib.unquote_plus(t)
                        tags = set((t,))
 
-       db = DB(data_path + '/db')
+       db = ArticleDB(os.path.join(data_path, 'db'))
        if atom:
                articles = db.get_articles(tags = tags)
                articles.sort(reverse = True)
@@ -601,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)
@@ -629,11 +847,12 @@ def handle_cmd():
                print "Error: article (%s) must be inside data_path (%s)" % \
                                (art_path, data_path)
                return 1
-       art_path = art_path[len(data_path):]
+       art_path = art_path[len(data_path)+1:]
 
-       if not os.path.isfile(data_path + '/db'):
-               open(data_path + '/db', 'w').write('')
-       db = DB(data_path + '/db')
+       db_filename = os.path.join(data_path, 'db')
+       if not os.path.isfile(db_filename):
+               open(db_filename, 'w').write('')
+       db = ArticleDB(db_filename)
 
        if cmd == 'add':
                article = Article(art_path, datetime.datetime.now(),