""" Storage for all your comments... """ import string import re import os from cStringIO import StringIO from wareweb import funcs import time from datetime import datetime import threading from commentary import svn from commentary import docformat __all__ = ['DocumentComments', 'acquire', 'release'] def comment_filename(path_info, pattern): parts = path_info.split('/') path_info_dir = '/'.join(parts[:-1]) path_info_file = parts[-1] or 'index.html' vars = { 'path_info_file': path_info_file, 'path_info_dir': path_info_dir, 'path_info': path_info, 'path_info_base': os.path.splitext(path_info_file)[0], 'path_info_ext': os.path.splitext(path_info_file)[1], } return string.Template(pattern).substitute(vars) _make_lock_lock = threading.Lock() _locks = {} def acquire(path_info): if path_info not in _locks: _make_lock_lock.acquire() try: if path_info not in _locks: _locks[path_info] = threading.Lock() finally: _make_lock_lock.release() _locks[path_info].acquire() def release(path_info): _locks[path_info].release() class DocumentComments(object): def __init__(self, path_info, filename_pattern): self.max_id = 1 self.path_info = path_info self.data_filename = comment_filename(path_info, filename_pattern) self._comments = {} self.id_order = [] if os.path.exists(self.data_filename): # @@: Should do locking somehow... self.read_data() self.svn = svn.SVNContext('Web updates') _section_start_re = re.compile(r'^\s*={4}=*\s*(.*?)\s*$') _comment_start_re = re.compile(r'^\s*-{4}-*\s*$') def read_data(self): f = open(self.data_filename) content = f.read().decode('utf8') lines = content.splitlines() f.close() current_id = None current_headers = {} in_headers = False body = None for line in lines: if current_id is None and not line.strip(): continue if line.startswith('#'): continue m = self._section_start_re.search(line) if m: if body is not None: self.add_comment( current_id, Comment(current_headers, ''.join(body))) body = None current_id = m.group(1) self.id_order.append(current_id) continue m = self._comment_start_re.search(line) if m: assert current_id is not None if body is not None: self.add_comment( current_id, Comment(current_headers, ''.join(body))) current_headers = {} body = [] in_headers = True continue if in_headers: if not line.strip(): in_headers = False continue if ':' not in line: raise ValueError( "Bad header line: %r" % line) name, value = line.split(':', 1) name = name.strip().lower() value = value.strip() current_headers[name] = value continue if line.startswith('.'): line = line[1:] body.append(line) if body is not None: self.add_comment( current_id, Comment(current_headers, ''.join(body))) _bad_starters = ('----', '====', '.', '#') def save(self): self.svn.ensure_dir(os.path.dirname(self.data_filename)) f = StringIO() for id in self.id_order: if not self._comments.get(id, []): continue f.write('==== %s\n' % id) for comment in self._comments.get(id, []): f.write('-'*40+'\n') for name, value in sorted(comment.headers.items()): if not value.strip(): continue assert len(value.splitlines()) == 1, ( "Bad embedded newline in header: %s: %r" % (name, value)) f.write('%s: %s\n' % (name, value)) f.write('\n') for line in comment.body.splitlines(): for bad in self._bad_starters: if line.startswith(bad): line = '.'+line break if isinstance(line, unicode): line = line.encode('utf8') f.write(line+'\n') text = f.getvalue() exists = os.path.exists(self.data_filename) if not text: if not exists: # Then we really don't care return self.svn.delete_file(self.data_filename) else: out = open(self.data_filename, 'w') out.write(text) out.close() self.svn.file_changed(self.data_filename, not exists) self.svn.commit() def add_comment(self, position_id, comment): position_id = position_id.strip() if position_id not in self.id_order: self.id_order.append(position_id) id = comment.headers.get('id') if not id: id = self.max_id comment.headers['id'] = str(id) self.max_id += 1 if int(id) >= self.max_id: self.max_id = int(id)+1 self._comments.setdefault(position_id, []).append( comment) def delete_comment(self, comment_id): done = False for comments in self._comments.values(): for comment in comments: if comment.headers.get('id') == comment_id: comments.remove(comment) done = True break if done: break if not done: raise KeyError( "No comment by the id %r found" % comment_id) def comment_by_id(self, comment_id): for comments in self._comments.values(): for comment in comments: if comment.headers.get('id') == comment_id: return comment raise KeyError( "No comment by the id %r found" % comment_id) def comments(self, position_id): return self._comments.get(position_id.strip(), []) def iter_comments(self): """ Iterate over all comments, returning (position_id, list_of_comments) """ for position_id in self.id_order: yield (position_id, self._comments.get(position_id, [])) class Comment(object): def __init__(self, headers, body): self.headers = headers self.body = body def html(self, base, **kw): #content = funcs.html_quote_whitespace(self.body).strip() content = docformat.convert_text(self.body) date = self.headers.get('date', '').strip() if date: try: date = time.strptime(date, '%Y-%m-%dT%H:%M:%S') except ValueError, e: date = 'Bad date: %s' % e else: date = datetime.fromtimestamp( time.mktime(date)) date = funcs.format_date_relative(date) vars = kw word_count = count_words(content) if word_count == 1: words = 'word' else: words = 'words' vars.update({ 'base': base, 'content': content, 'date_fmt': date, 'word_count': word_count, 'words': words, }) vars.update(self.headers) for k, default in [('username', 'Anonymous')]: if not vars.get(k): vars[k] = default if self.headers.get('immutable'): tmpl = immutable_comment_template else: tmpl = comment_template return tmpl % vars def __repr__(self): return '<%s body=%r>' % ( self.__class__.__name__, self.body[:20]) def count_words(html): html = re.sub('<.*?>', '', html) return len(html.split()) comment_template = '''
By: %(username)s  %(date_fmt)s
%(content)s
''' immutable_comment_template = '''
By: %(username)s  %(date_fmt)s
%(content)s
'''